diff --git a/forge.config.ts b/forge.config.ts index 385b02f96..bb1399833 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -62,9 +62,6 @@ const config: ForgeConfig = { './resources/json_output.py', './resources/entrypoint.js', './resources/replay.js', - './resources/splashscreen.html', - './resources/logo-splashscreen-dark.svg', - './resources/logo-splashscreen.svg', ...getPlatformSpecificResources(), ], windowsSign, diff --git a/index.html b/index.html index d2971bd60..779af4099 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,37 @@ Grafana k6 Studio -
+
+
+
+
+
+ diff --git a/resources/logo-splashscreen-dark.svg b/resources/logo-splashscreen-dark.svg deleted file mode 100644 index 648edf528..000000000 --- a/resources/logo-splashscreen-dark.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/resources/logo-splashscreen.svg b/resources/logo-splashscreen.svg deleted file mode 100644 index fc5097e3a..000000000 --- a/resources/logo-splashscreen.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/resources/splashscreen.html b/resources/splashscreen.html deleted file mode 100644 index d008f0b66..000000000 --- a/resources/splashscreen.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - Grafana k6 Studio - - - - - - - - diff --git a/src/App.tsx b/src/App.tsx index 9e9ae2895..5a8610976 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import { DevToolsDialog } from '@/components/DevToolsDialog' import { SettingsDialog } from '@/components/Settings/SettingsDialog' import { Toasts } from '@/components/Toast/Toasts' import { Theme as StudioTheme } from '@/components/primitives/Theme' -import { useCloseSplashScreen } from '@/hooks/useCloseSplashScreen' import { useTheme } from '@/hooks/useTheme' import { queryClient } from '@/utils/query' @@ -18,7 +17,6 @@ enableMapSet() export function App() { const theme = useTheme() - useCloseSplashScreen() return ( diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index c5221d75e..69311ae2c 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -1,4 +1,5 @@ import log from 'electron-log/renderer' +import { lazy } from 'react' import { Navigate, Route, @@ -9,16 +10,35 @@ import { } from 'react-router-dom' import { Layout } from '@/components/Layout/Layout' -import { BrowserTestEditor } from '@/views/BrowserTestEditor' -import { Home } from '@/views/Home' -import { Recorder } from '@/views/Recorder' -import { RecordingPreviewer } from '@/views/RecordingPreviewer' -import { Validator } from '@/views/Validator' import { ErrorElement } from './ErrorElement' import { routeMap } from './routeMap' -import { DataFile } from './views/DataFile' -import { Generator } from './views/Generator' + +const Home = lazy(() => + import('@/views/Home').then((module) => ({ default: module.Home })) +) +const Recorder = lazy(() => + import('@/views/Recorder').then((module) => ({ default: module.Recorder })) +) +const RecordingPreviewer = lazy(() => + import('@/views/RecordingPreviewer').then((module) => ({ + default: module.RecordingPreviewer, + })) +) +const Generator = lazy(() => + import('@/views/Generator').then((module) => ({ default: module.Generator })) +) +const BrowserTestEditor = lazy(() => + import('@/views/BrowserTestEditor').then((module) => ({ + default: module.BrowserTestEditor, + })) +) +const Validator = lazy(() => + import('@/views/Validator').then((module) => ({ default: module.Validator })) +) +const DataFile = lazy(() => + import('@/views/DataFile').then((module) => ({ default: module.DataFile })) +) const router = createHashRouter( createRoutesFromChildren( diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index b6ec2d64e..96fe66a26 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,16 +1,34 @@ import { css } from '@emotion/react' -import { Box, IconButton } from '@radix-ui/themes' +import { Box, Flex, IconButton, Spinner } from '@radix-ui/themes' import { Allotment } from 'allotment' import { PanelLeftOpenIcon } from 'lucide-react' -import { useEffect } from 'react' +import { Suspense, useEffect } from 'react' import { Outlet, useLocation } from 'react-router-dom' import { useLocalStorage } from 'react-use' +import { useDelayedVisibility } from '@/hooks/useDelayedVisibility' import { useListenDeepLinks } from '@/hooks/useListenDeepLinks' import { ActivityBar } from './ActivityBar' import { Sidebar } from './Sidebar' +function RouteLoadingFallback() { + const showSpinner = useDelayedVisibility() + + return ( + + {showSpinner && } + + ) +} + export function Layout() { const [isSidebarExpanded, setIsSidebarExpanded] = useLocalStorage( 'isSidebarExpanded', @@ -78,7 +96,9 @@ export function Layout() { - + }> + + diff --git a/src/components/Layout/View.tsx b/src/components/Layout/View.tsx index 6beeb07db..a733da5f1 100644 --- a/src/components/Layout/View.tsx +++ b/src/components/Layout/View.tsx @@ -1,22 +1,13 @@ import { css } from '@emotion/react' import { Flex, Spinner } from '@radix-ui/themes' -import { PropsWithChildren, ReactNode, useEffect, useState } from 'react' +import { PropsWithChildren, ReactNode } from 'react' + +import { useDelayedVisibility } from '@/hooks/useDelayedVisibility' import { ViewHeading } from './ViewHeading' function LoadingSpinner() { - const [showSpinner, setShowSpinner] = useState(false) - - useEffect(() => { - // Only show the spinner if loading takes more than 50ms to avoid flickering - const timeout = setTimeout(() => { - setShowSpinner(true) - }, 50) - - return () => { - clearTimeout(timeout) - } - }, []) + const showSpinner = useDelayedVisibility() return ( - {showSpinner && ( - - )} + {showSpinner && } ) } diff --git a/src/handlers/app/index.ts b/src/handlers/app/index.ts index 7e42e6a06..4878ce72b 100644 --- a/src/handlers/app/index.ts +++ b/src/handlers/app/index.ts @@ -1,6 +1,5 @@ import { ipcMain, app } from 'electron' -import { showWindow } from '@/main/window' import { trackEvent } from '@/services/usageTracking' import { UsageEvent } from '@/services/usageTracking/types' import { browserWindowFromEvent } from '@/utils/electron' @@ -24,20 +23,6 @@ export function initialize() { browserWindow.close() }) - ipcMain.on(AppHandler.SplashscreenClose, (event) => { - console.log(`${AppHandler.SplashscreenClose} event received`) - - const browserWindow = browserWindowFromEvent(event) - - if ( - k6StudioState.splashscreenWindow && - !k6StudioState.splashscreenWindow.isDestroyed() - ) { - k6StudioState.splashscreenWindow.close() - showWindow(browserWindow) - } - }) - ipcMain.on(AppHandler.TrackEvent, (_, event: UsageEvent) => { trackEvent(event) }) diff --git a/src/handlers/app/preload.ts b/src/handlers/app/preload.ts index 562452229..c4ce4571b 100644 --- a/src/handlers/app/preload.ts +++ b/src/handlers/app/preload.ts @@ -8,10 +8,6 @@ import { AppHandler } from './types' export const platform = process.platform -export function closeSplashscreen() { - ipcRenderer.send(AppHandler.SplashscreenClose) -} - export function onApplicationClose(callback: () => void) { return createListener(AppHandler.Close, callback) } @@ -28,6 +24,26 @@ export function trackEvent(event: UsageEvent) { return ipcRenderer.send(AppHandler.TrackEvent, event) } +let pendingDeepLink: string | null = null +let deepLinkCallback: ((url: string) => void) | null = null + +ipcRenderer.on(AppHandler.Navigate, (_, url: string) => { + if (deepLinkCallback) { + deepLinkCallback(url) + } else { + pendingDeepLink = url + } +}) + export function onDeepLink(callback: (url: string) => void) { - return createListener(AppHandler.DeepLink, callback) + deepLinkCallback = callback + + if (pendingDeepLink) { + callback(pendingDeepLink) + pendingDeepLink = null + } + + return () => { + deepLinkCallback = null + } } diff --git a/src/handlers/app/types.ts b/src/handlers/app/types.ts index af05e8768..d95326c34 100644 --- a/src/handlers/app/types.ts +++ b/src/handlers/app/types.ts @@ -1,7 +1,6 @@ export enum AppHandler { Close = 'app:close', ChangeRoute = 'app:change-route', - SplashscreenClose = 'app:splashscreen-close', - DeepLink = 'app:deep-link', + Navigate = 'app:navigate', TrackEvent = 'app:track-event', } diff --git a/src/hooks/useCloseSplashScreen.test.ts b/src/hooks/useCloseSplashScreen.test.ts deleted file mode 100644 index c7650dcbb..000000000 --- a/src/hooks/useCloseSplashScreen.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { renderHook } from '@testing-library/react' -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' - -import { useCloseSplashScreen } from './useCloseSplashScreen' - -const closeSplashscreen = vi.fn() - -beforeAll(() => { - vi.stubGlobal('studio', { - app: { - closeSplashscreen, - }, - }) -}) - -describe('useCloseSplashScreen', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should call closeSplashscreen on mount', () => { - renderHook(() => useCloseSplashScreen()) - - expect(closeSplashscreen).toHaveBeenCalled() - }) -}) diff --git a/src/hooks/useCloseSplashScreen.ts b/src/hooks/useCloseSplashScreen.ts deleted file mode 100644 index d2fde0d06..000000000 --- a/src/hooks/useCloseSplashScreen.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useEffect } from 'react' - -export function useCloseSplashScreen() { - useEffect(() => { - window.studio.app.closeSplashscreen() - }, []) -} diff --git a/src/hooks/useDelayedVisibility.ts b/src/hooks/useDelayedVisibility.ts new file mode 100644 index 000000000..0cfc327ed --- /dev/null +++ b/src/hooks/useDelayedVisibility.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react' + +/** + * Hook that delays showing a component until a specified delay has passed. + * Useful for preventing flickering of loading states for quick operations. + * + * @param delayMs The delay in milliseconds before showing the component + * @returns A boolean indicating whether the component should be visible + */ +export function useDelayedVisibility(delayMs = 50) { + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + const timeout = setTimeout(() => { + setIsVisible(true) + }, delayMs) + + return () => { + clearTimeout(timeout) + } + }, [delayMs]) + + return isVisible +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 117cac88f..b4167b353 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,7 @@ import { updateElectronApp } from 'update-electron-app' import * as handlers from './handlers' import { ProxyHandler } from './handlers/proxy/types' -import { initializeDeepLinks } from './main/deepLinks' +import { initializeDeepLinks, replayPendingDeepLink } from './main/deepLinks' import * as mainState from './main/k6StudioState' import { initializeLogger } from './main/logger' import { configureApplicationMenu } from './main/menu' @@ -54,47 +54,6 @@ handlers.initialize() mainState.initialize() initializeDeepLinks() -const createSplashWindow = async () => { - k6StudioState.splashscreenWindow = new BrowserWindow({ - width: 600, - height: 400, - frame: false, - show: false, - alwaysOnTop: true, - }) - - let splashscreenFile: string - - // if we are in dev server we take resources directly, otherwise look in the app resources folder. - if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - splashscreenFile = path.join( - app.getAppPath(), - 'resources', - 'splashscreen.html' - ) - } else { - splashscreenFile = path.join(process.resourcesPath, 'splashscreen.html') - } - - // Open the DevTools. - if (process.env.NODE_ENV === 'development') { - k6StudioState.splashscreenWindow.webContents.openDevTools({ - mode: 'detach', - }) - } - - // wait for the window to be ready before showing it. It prevents showing a white page on longer load times. - k6StudioState.splashscreenWindow.once('ready-to-show', () => { - if (k6StudioState.splashscreenWindow) { - k6StudioState.splashscreenWindow.show() - } - }) - - await k6StudioState.splashscreenWindow.loadFile(splashscreenFile) - - return k6StudioState.splashscreenWindow -} - const createWindow = async () => { const icon = getAppIcon(process.env.NODE_ENV === 'development') if (getPlatform() === 'mac') { @@ -125,6 +84,10 @@ const createWindow = async () => { }, }) + mainWindow.once('ready-to-show', () => { + showWindow(mainWindow) + }) + configureApplicationMenu() configureWatcher(mainWindow) k6StudioState.wasAppClosedByClient = false @@ -141,7 +104,6 @@ const createWindow = async () => { k6StudioState.currentProxyProcess = await launchProxyAndAttachEmitter(mainWindow) - // and load the index.html of the app. if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { await mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) } else { @@ -150,9 +112,10 @@ const createWindow = async () => { ) } - // Open the DevTools. + replayPendingDeepLink() + if (process.env.NODE_ENV === 'development') { - mainWindow.webContents.openDevTools({ mode: 'detach' }) + mainWindow.webContents.openDevTools() } mainWindow.on('closed', () => @@ -186,7 +149,6 @@ app.whenReady().then( await initSettings() k6StudioState.appSettings = await getSettings() nativeTheme.themeSource = k6StudioState.appSettings.appearance.theme - await createSplashWindow() await setupProjectStructure() await initEventTracking() @@ -213,8 +175,8 @@ app.on('activate', async () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { - const mainWindow = await createWindow() - showWindow(mainWindow) + await createWindow() + // Window is already shown by the 'ready-to-show' event handler } }) diff --git a/src/main/deepLinks.ts b/src/main/deepLinks.ts index 7e9c0f826..4680c0310 100644 --- a/src/main/deepLinks.ts +++ b/src/main/deepLinks.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain } from 'electron' +import { app, BrowserWindow } from 'electron' import log from 'electron-log/main' import path from 'path' @@ -31,14 +31,13 @@ function listenMacOsDeepLink() { app.on('open-url', (_, url) => { handleDeepLink(url) }) +} - // Handle the case when the app is launched with a custom protocol link - ipcMain.on(AppHandler.SplashscreenClose, () => { - if (deepLinkUrl) { - handleDeepLink(deepLinkUrl) - deepLinkUrl = null - } - }) +export function replayPendingDeepLink() { + if (deepLinkUrl) { + handleDeepLink(deepLinkUrl) + deepLinkUrl = null + } } // Windows and linux emit second-instance event rather than the open-url event , @@ -74,7 +73,7 @@ function listenWindowsDeepLink() { function handleDeepLink(url: string) { const mainWindow = BrowserWindow.getAllWindows()[0] - // Main window not ready yet, store the URL until splash screen is closed + // Main window not ready yet, store the URL until the renderer has loaded if (!mainWindow) { deepLinkUrl = url return @@ -89,7 +88,7 @@ function handleDeepLink(url: string) { const path = parsedUrl.searchParams.get('path') - mainWindow.webContents.send(AppHandler.DeepLink, path) + mainWindow.webContents.send(AppHandler.Navigate, path) // Restore and focus the main window, needed for windows if (mainWindow.isMinimized()) { diff --git a/src/main/k6StudioState.ts b/src/main/k6StudioState.ts index 75f4ed184..6b01f493a 100644 --- a/src/main/k6StudioState.ts +++ b/src/main/k6StudioState.ts @@ -1,5 +1,4 @@ import { FSWatcher } from 'chokidar' -import { BrowserWindow } from 'electron' import eventEmitter from 'events' import { RecordingSession } from '@/handlers/browser/recorders/types' @@ -21,7 +20,6 @@ export type k6StudioState = { appShuttingDown: boolean currentClientRoute: string wasAppClosedByClient: boolean - splashscreenWindow: BrowserWindow | null watcher: FSWatcher | null } @@ -43,8 +41,6 @@ export function initialize() { // Used to track the current route in the client side currentClientRoute: '/', wasAppClosedByClient: false, - - splashscreenWindow: null, watcher: null, } } diff --git a/src/main/window.ts b/src/main/window.ts index fac8dcec9..b49752310 100644 --- a/src/main/window.ts +++ b/src/main/window.ts @@ -1,4 +1,4 @@ -import { BrowserWindow } from 'electron' +import { app, BrowserWindow } from 'electron' import log from 'electron-log/main' import { saveSettings } from './settings' @@ -10,7 +10,7 @@ export function showWindow(browserWindow: BrowserWindow) { } else { browserWindow.show() } - browserWindow.focus() + app.focus({ steal: true }) } export async function trackWindowState(browserWindow: BrowserWindow) {