diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9bdb7e3c..47a4e076 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -7,7 +7,7 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', 'plugin:import/recommended', 'plugin:import/electron', 'plugin:import/typescript', @@ -18,7 +18,11 @@ module.exports = { 'prettier', ], parser: '@typescript-eslint/parser', - ignorePatterns: ['resources/group_snippet.js', 'install-k6.js'], + ignorePatterns: [ + 'resources/group_snippet.js', + 'install-k6.js', + '.eslintrc.cjs', + ], plugins: [ 'import', 'unused-imports', @@ -43,11 +47,16 @@ module.exports = { argsIgnorePattern: '^_', }, ], - // TODO: remove warnings when plugin:@typescript-eslint/recommended-type-checked is enabled - '@typescript-eslint/no-unsafe-assignment': 'warn', - '@typescript-eslint/no-unsafe-return': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', - '@typescript-eslint/no-unsafe-member-access': 'warn', + '@typescript-eslint/no-floating-promises': [ + 'error', + { + ignoreIIFE: true, + }, + ], + '@typescript-eslint/no-misused-promises': [ + 'error', + { checksVoidReturn: false }, + ], }, parserOptions: { diff --git a/package-lock.json b/package-lock.json index 17ebc3fa..6f9d354e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11345,9 +11345,9 @@ "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", diff --git a/src/ErrorElement.tsx b/src/ErrorElement.tsx index 2434ef1a..cf05abae 100644 --- a/src/ErrorElement.tsx +++ b/src/ErrorElement.tsx @@ -10,11 +10,10 @@ import { css } from '@emotion/react' import GrotCrashed from '@/assets/grot-crashed.svg' import { ArrowLeftIcon } from '@radix-ui/react-icons' -const handleCreateIssue = () => { +const handleCreateIssue = () => window.studio.browser.openExternalLink( 'https://github.com/grafana/k6-studio/issues' ) -} const handleOpenLogs = () => { window.studio.log.openLogFolder() diff --git a/src/browser.ts b/src/browser.ts index 9ca5bd6c..83a7744d 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -70,7 +70,7 @@ export const launchBrowser = async ( `--proxy-server=http://localhost:${appSettings.proxy.port}`, `--ignore-certificate-errors-spki-list=${certificateSPKI}`, disableChromeOptimizations, - url ?? '', + url?.trim() || 'about:blank', ], onExit: sendBrowserClosedEvent, }) diff --git a/src/codegen/codegen.utils.ts b/src/codegen/codegen.utils.ts index c8089031..5a260801 100644 --- a/src/codegen/codegen.utils.ts +++ b/src/codegen/codegen.utils.ts @@ -24,6 +24,8 @@ export function stringify(value: unknown): string { return `{${properties}}` } + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `${value}` } diff --git a/src/components/ExperimentalBanner.tsx b/src/components/ExperimentalBanner.tsx index 294fc01c..2a6145a0 100644 --- a/src/components/ExperimentalBanner.tsx +++ b/src/components/ExperimentalBanner.tsx @@ -1,11 +1,10 @@ import { ExclamationTriangleIcon } from '@radix-ui/react-icons' import { Flex, Text, Link } from '@radix-ui/themes' -const handleLinkClick = () => { +const handleLinkClick = () => window.studio.browser.openExternalLink( 'https://github.com/grafana/k6-studio/issues' ) -} export function ExperimentalBanner() { return ( diff --git a/src/components/Form/FileUploadInput.tsx b/src/components/Form/FileUploadInput.tsx index ac7e1c77..d9cab6df 100644 --- a/src/components/Form/FileUploadInput.tsx +++ b/src/components/Form/FileUploadInput.tsx @@ -39,6 +39,8 @@ export const FileUploadInput = ({ disabled={disabled} onChange={field.onChange} name={field.name} + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment value={field.value} /> diff --git a/src/components/Layout/ActivityBar/HelpButton.tsx b/src/components/Layout/ActivityBar/HelpButton.tsx index b0caa5e1..fe38730a 100644 --- a/src/components/Layout/ActivityBar/HelpButton.tsx +++ b/src/components/Layout/ActivityBar/HelpButton.tsx @@ -2,17 +2,15 @@ import { QuestionMarkCircledIcon } from '@radix-ui/react-icons' import { DropdownMenu, IconButton, Tooltip } from '@radix-ui/themes' export function HelpButton() { - const handleOpenDocs = () => { + const handleOpenDocs = () => window.studio.browser.openExternalLink( 'https://grafana.com/docs/k6-studio/' ) - } - const handleReportIssue = () => { + const handleReportIssue = () => window.studio.browser.openExternalLink( 'https://github.com/grafana/k6-studio/issues' ) - } const handleOpenApplicationLogs = () => { window.studio.log.openLogFolder() diff --git a/src/components/Layout/ActivityBar/ProxyStatusIndicator.tsx b/src/components/Layout/ActivityBar/ProxyStatusIndicator.tsx index b1b6a483..7521a709 100644 --- a/src/components/Layout/ActivityBar/ProxyStatusIndicator.tsx +++ b/src/components/Layout/ActivityBar/ProxyStatusIndicator.tsx @@ -18,11 +18,10 @@ export function ProxyStatusIndicator() { const setProxyStatus = useStudioUIStore((state) => state.setProxyStatus) useEffect(() => { - async function fetchProxyStatus() { + ;(async function fetchProxyStatus() { const status = await window.studio.proxy.getProxyStatus() setProxyStatus(status) - } - fetchProxyStatus() + })() return window.studio.proxy.onProxyStatusChange((status) => setProxyStatus(status) diff --git a/src/components/Layout/Sidebar/Sidebar.hooks.ts b/src/components/Layout/Sidebar/Sidebar.hooks.ts index dc71dbc5..fc0ab5d8 100644 --- a/src/components/Layout/Sidebar/Sidebar.hooks.ts +++ b/src/components/Layout/Sidebar/Sidebar.hooks.ts @@ -26,13 +26,14 @@ function useFolderContent() { const setFolderContent = useStudioUIStore((s) => s.setFolderContent) useEffect(() => { - window.studio.ui.getFiles().then((files) => { + ;(async () => { + const files = await window.studio.ui.getFiles() setFolderContent({ recordings: toFileMap(files.recordings), generators: toFileMap(files.generators), scripts: toFileMap(files.scripts), }) - }) + })() }, [setFolderContent]) useEffect( diff --git a/src/components/Layout/Sidebar/Sidebar.tsx b/src/components/Layout/Sidebar/Sidebar.tsx index 915c2a79..2babb372 100644 --- a/src/components/Layout/Sidebar/Sidebar.tsx +++ b/src/components/Layout/Sidebar/Sidebar.tsx @@ -28,78 +28,81 @@ export function Sidebar({ isExpanded, onCollapseSidebar }: SidebarProps) { maxWidth="100%" overflow="hidden" position="relative" + asChild > - - - - {isExpanded && ( - + + - - - )} - - - - - + onChange={setSearchTerm} + /> + + {isExpanded && ( + + + + )} + + + + + + + + + + + + + } + /> + - - - + - - } - /> - - - - - - } - /> - - - + } + /> + + + + ) } diff --git a/src/components/Settings/LogsSettings.tsx b/src/components/Settings/LogsSettings.tsx index 34b1c637..d5ea034a 100644 --- a/src/components/Settings/LogsSettings.tsx +++ b/src/components/Settings/LogsSettings.tsx @@ -17,13 +17,11 @@ export function LogsSettings() { // retrieve the current content of the log file useEffect(() => { - async function fetchLogContent() { + ;(async function fetchLogContent() { const content = await window.studio.log.getLogContent() setLogContent(content) scrollToLastLine() - } - - fetchLogContent() + })() }, [scrollToLastLine]) // subscribe to log changes @@ -45,14 +43,14 @@ export function LogsSettings() { return ( - + - - k6 Studio logs in this screen are updated in real-time. + + Application logs are updated in real-time. diff --git a/src/components/Settings/Settings.hooks.ts b/src/components/Settings/Settings.hooks.ts new file mode 100644 index 00000000..56523987 --- /dev/null +++ b/src/components/Settings/Settings.hooks.ts @@ -0,0 +1,26 @@ +import { queryClient } from '@/utils/query' +import { useMutation, useQuery } from '@tanstack/react-query' + +export function useSettings() { + return useQuery({ + queryKey: ['settings'], + queryFn: window.studio.settings.getSettings, + }) +} + +export function useSaveSettings(onSuccess?: () => void) { + return useMutation({ + mutationFn: window.studio.settings.saveSettings, + onSuccess: async (isSuccessful) => { + if (!isSuccessful) { + return + } + + await queryClient.invalidateQueries({ queryKey: ['settings'] }) + onSuccess?.() + }, + onError: (error) => { + console.error('Error saving settings', error) + }, + }) +} diff --git a/src/components/Settings/SettingsDialog.tsx b/src/components/Settings/SettingsDialog.tsx index bea6ff6f..76189eca 100644 --- a/src/components/Settings/SettingsDialog.tsx +++ b/src/components/Settings/SettingsDialog.tsx @@ -3,7 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { Box, Button, Dialog, Flex, ScrollArea, Tabs } from '@radix-ui/themes' import { ExclamationTriangleIcon } from '@radix-ui/react-icons' import { findIndex, sortBy } from 'lodash-es' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { ProxySettings } from './ProxySettings' @@ -14,6 +14,7 @@ import { UsageReportSettings } from './UsageReportSettings' import { ButtonWithTooltip } from '../ButtonWithTooltip' import { AppearanceSettings } from './AppearanceSettings' import { LogsSettings } from './LogsSettings' +import { useSaveSettings, useSettings } from './Settings.hooks' type SettingsDialogProps = { open: boolean @@ -41,18 +42,12 @@ const tabs = [ ] export const SettingsDialog = ({ open, onOpenChange }: SettingsDialogProps) => { - const [settings, setSettings] = useState() - const [submitting, setSubmitting] = useState(false) + const { data: settings } = useSettings() + const { mutateAsync: saveSettings, isPending } = useSaveSettings(() => { + onOpenChange(false) + }) const [selectedTab, setSelectedTab] = useState('proxy') - useEffect(() => { - async function fetchSettings() { - const data = await window.studio.settings.getSettings() - setSettings(data) - } - fetchSettings() - }, []) - const formMethods = useForm({ resolver: zodResolver(AppSettingsSchema), shouldFocusError: true, @@ -65,23 +60,6 @@ export const SettingsDialog = ({ open, onOpenChange }: SettingsDialogProps) => { formState: { isDirty, errors }, } = formMethods - const onSubmit = async (data: AppSettings) => { - try { - setSubmitting(true) - const isSuccess = await window.studio.settings.saveSettings(data) - if (isSuccess) { - onOpenChange(false) - reset(data) - setSettings(data) - setSelectedTab('proxy') - } - } catch (error) { - console.error('Error saving settings', error) - } finally { - setSubmitting(false) - } - } - const onInvalid = (errors: Record) => { // Sort tabs by the order they appear in the UI const tabsWithError = sortBy(Object.keys(errors), (key) => @@ -159,10 +137,13 @@ export const SettingsDialog = ({ open, onOpenChange }: SettingsDialogProps) => { saveSettings(data), + onInvalid + )} > Save changes diff --git a/src/components/Settings/UsageReportSettings.tsx b/src/components/Settings/UsageReportSettings.tsx index 170e827b..3ff2a9aa 100644 --- a/src/components/Settings/UsageReportSettings.tsx +++ b/src/components/Settings/UsageReportSettings.tsx @@ -6,11 +6,10 @@ import { AppSettings } from '@/types/settings' export const UsageReportSettings = () => { const { control, register } = useFormContext() - const handleLinkClick = () => { + const handleLinkClick = () => window.studio.browser.openExternalLink( 'https://grafana.com/docs/k6-studio/set-up/usage-collection/' ) - } return ( diff --git a/src/components/WebLogView/Group.tsx b/src/components/WebLogView/Group.tsx index 24b42edc..46220f9a 100644 --- a/src/components/WebLogView/Group.tsx +++ b/src/components/WebLogView/Group.tsx @@ -95,7 +95,7 @@ export function Group({ useClickAway(headerRef, () => { if (!group.isEditing) return - handleSubmit(submit)() + return handleSubmit(submit)() }) const isValidName = (value: string) => { diff --git a/src/components/WebLogView/RequestDetails/utils.ts b/src/components/WebLogView/RequestDetails/utils.ts index e5361551..aaf3eb3b 100644 --- a/src/components/WebLogView/RequestDetails/utils.ts +++ b/src/components/WebLogView/RequestDetails/utils.ts @@ -28,6 +28,8 @@ export function parseParams(data: ProxyData) { } return stringify( + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument JSON.parse(jsonrepair(parsePythonByteString(contentDecoded))) ) } catch (e) { diff --git a/src/components/WebLogView/ResponseDetails/Font.tsx b/src/components/WebLogView/ResponseDetails/Font.tsx index 17f387c3..0310f57b 100644 --- a/src/components/WebLogView/ResponseDetails/Font.tsx +++ b/src/components/WebLogView/ResponseDetails/Font.tsx @@ -6,19 +6,17 @@ export function Font({ url }: { url: string }) { const [isReady, setIsReady] = useState(false) const [name, setName] = useState('') - async function addFontFace(url: string) { - const name = uniqueId('font-') - const font = new FontFace(name, `url(${url})`) - - await font.load() - document.fonts.add(font) - setIsReady(true) - setName(name) - } - useEffect(() => { - setIsReady(false) - addFontFace(url) + ;(async function addFontFace(url: string) { + setIsReady(false) + const name = uniqueId('font-') + const font = new FontFace(name, `url(${url})`) + + await font.load() + document.fonts.add(font) + setIsReady(true) + setName(name) + })(url) }, [url]) if (!isReady) { diff --git a/src/components/WebLogView/ResponseDetails/Preview.tsx b/src/components/WebLogView/ResponseDetails/Preview.tsx index 98742e14..d66c2765 100644 --- a/src/components/WebLogView/ResponseDetails/Preview.tsx +++ b/src/components/WebLogView/ResponseDetails/Preview.tsx @@ -53,6 +53,8 @@ export function Preview({ content, contentType, format }: PreviewProps) { return ( field.name !== 'root'} + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment src={JSON.parse(content)} theme={theme === 'dark' ? 'monokai' : 'rjv-default'} style={theme === 'dark' ? reactJsonDarkStyles : reactJsonLightStyles} diff --git a/src/components/WebLogView/ResponseDetails/ResponseDetails.utils.ts b/src/components/WebLogView/ResponseDetails/ResponseDetails.utils.ts index 2b119650..03b8581c 100644 --- a/src/components/WebLogView/ResponseDetails/ResponseDetails.utils.ts +++ b/src/components/WebLogView/ResponseDetails/ResponseDetails.utils.ts @@ -49,8 +49,12 @@ export function parseContent(format: string | undefined, data: ProxyData) { try { switch (format) { case 'json': + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return stringify(JSON.parse(safeAtob(content))) case 'json-raw': + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return stringify(JSON.parse(safeAtob(content)), 0) case 'css': case 'html': diff --git a/src/components/WebLogView/Row.tsx b/src/components/WebLogView/Row.tsx index eba46f48..d308aa7f 100644 --- a/src/components/WebLogView/Row.tsx +++ b/src/components/WebLogView/Row.tsx @@ -6,6 +6,7 @@ import { MethodBadge } from '../MethodBadge' import { ResponseStatusBadge } from '../ResponseStatusBadge' import { TableCellWithTooltip } from '../TableCellWithTooltip' import { HighlightedText } from '../HighlightedText' +import { getRequestType } from './WebLogView.utils' interface RowProps { data: ProxyDataWithMatches @@ -53,6 +54,7 @@ export function Row({ data, isSelected, onSelectRequest }: RowProps) { /> + {getRequestType(data)} diff --git a/src/components/WebLogView/WebLogView.tsx b/src/components/WebLogView/WebLogView.tsx index 7c66e475..3ba5b15b 100644 --- a/src/components/WebLogView/WebLogView.tsx +++ b/src/components/WebLogView/WebLogView.tsx @@ -93,6 +93,7 @@ function RequestList({ Method Status + Type Host Path diff --git a/src/components/WebLogView/WebLogView.utils.ts b/src/components/WebLogView/WebLogView.utils.ts index 50ac307a..1a0d4220 100644 --- a/src/components/WebLogView/WebLogView.utils.ts +++ b/src/components/WebLogView/WebLogView.utils.ts @@ -1,4 +1,6 @@ import { GroupedProxyData, ProxyData } from '@/types' +import { getContentType } from '@/utils/headers' +import { isNonStaticAssetResponse } from '@/utils/staticAssets' export function removeQueryStringFromUrl(url: string) { return url.split('?')[0] @@ -20,3 +22,12 @@ export function isGroupedProxyData( ): data is GroupedProxyData { return !Array.isArray(data) } + +export function getRequestType(data: ProxyData) { + if (isNonStaticAssetResponse(data)) { + return 'fetch' + } + + const mimeType = getContentType(data.response?.headers ?? []) + return mimeType ? mimeType.split('/')[1] : 'unknown' +} diff --git a/src/hooks/useRunChecks.test.ts b/src/hooks/useRunChecks.test.ts index b9a6ffab..d3d87b05 100644 --- a/src/hooks/useRunChecks.test.ts +++ b/src/hooks/useRunChecks.test.ts @@ -41,7 +41,7 @@ describe('useRunChecks', () => { createK6Check({ id: '1', name: 'Check 1' }), createK6Check({ id: '2', name: 'Check 2' }), ] - onScriptCheck.mockImplementation((callback) => { + onScriptCheck.mockImplementation((callback: (data: K6Check[]) => void) => { callback(mockChecks) return () => {} }) diff --git a/src/hooks/useRunLogs.test.ts b/src/hooks/useRunLogs.test.ts index 17946996..7f85d76a 100644 --- a/src/hooks/useRunLogs.test.ts +++ b/src/hooks/useRunLogs.test.ts @@ -3,6 +3,7 @@ import { useRunLogs } from './useRunLogs' import { renderHook } from '@testing-library/react' import { act } from 'react' import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { K6Log } from '@/types' const onScriptLog = vi.fn() @@ -37,7 +38,7 @@ describe('useRunLogs', () => { it('should update logs when onScriptLog is called', () => { const mockLog = createK6Log() - onScriptLog.mockImplementation((callback) => { + onScriptLog.mockImplementation((callback: (log: K6Log) => void) => { callback(mockLog) return () => {} }) diff --git a/src/hooks/useScriptPreview.test.ts b/src/hooks/useScriptPreview.test.ts index b22506f7..9755ee66 100644 --- a/src/hooks/useScriptPreview.test.ts +++ b/src/hooks/useScriptPreview.test.ts @@ -16,7 +16,7 @@ import { } from '@/test/factories/generator' vi.mock('lodash-es', () => ({ - debounce: vi.fn((fn) => fn), + debounce: vi.fn((fn: () => void) => fn), })) vi.mock('@/store/generator', () => ({ useGeneratorStore: { diff --git a/src/hooks/useScriptPreview.ts b/src/hooks/useScriptPreview.ts index c68eaf7d..7b20e2b9 100644 --- a/src/hooks/useScriptPreview.ts +++ b/src/hooks/useScriptPreview.ts @@ -30,6 +30,8 @@ export function useScriptPreview() { }, 100) // Initial preview generation + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-floating-promises updatePreview(useGeneratorStore.getState()) const unsubscribe = useGeneratorStore.subscribe((state) => diff --git a/src/main.ts b/src/main.ts index 9c4ad33a..6beec69f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -113,7 +113,7 @@ const createSplashWindow = async () => { splashscreenWindow.webContents.openDevTools({ mode: 'detach' }) } - splashscreenWindow.loadFile(splashscreenFile) + await splashscreenWindow.loadFile(splashscreenFile) // wait for the window to be ready before showing it. It prevents showing a white page on longer load times. splashscreenWindow.once('ready-to-show', () => { @@ -158,9 +158,9 @@ const createWindow = async () => { // and load the index.html of the app. if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { - mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) + await mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL) } else { - mainWindow.loadFile( + await mainWindow.loadFile( path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`) ) } @@ -195,16 +195,21 @@ const createWindow = async () => { return mainWindow } -app.whenReady().then(async () => { - await initSettings() - appSettings = await getSettings() - nativeTheme.themeSource = appSettings.appearance.theme +app.whenReady().then( + async () => { + await initSettings() + appSettings = await getSettings() + nativeTheme.themeSource = appSettings.appearance.theme - await sendReport(appSettings.usageReport) - await createSplashWindow() - await setupProjectStructure() - await createWindow() -}) + await sendReport(appSettings.usageReport) + await createSplashWindow() + await setupProjectStructure() + await createWindow() + }, + (error) => { + log.error(error) + } +) // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits @@ -228,14 +233,14 @@ app.on('before-quit', async () => { // stop watching files to avoid crash on exit appShuttingDown = true await watcher.close() - stopProxyProcess() + return stopProxyProcess() }) -ipcMain.handle('app:change-route', async (_, route: string) => { +ipcMain.on('app:change-route', (_, route: string) => { currentClientRoute = route }) -ipcMain.handle('app:close', (event) => { +ipcMain.on('app:close', (event) => { console.log('app:close event received') wasAppClosedByClient = true @@ -255,9 +260,9 @@ ipcMain.handle('proxy:start', async (event) => { currentProxyProcess = await launchProxyAndAttachEmitter(browserWindow) }) -ipcMain.on('proxy:stop', async () => { +ipcMain.on('proxy:stop', () => { console.info('proxy:stop event received') - stopProxyProcess() + return stopProxyProcess() }) const waitForProxy = async (): Promise => { @@ -286,7 +291,7 @@ ipcMain.handle('browser:start', async (event, url?: string) => { ipcMain.on('browser:stop', async () => { console.info('browser:stop event received') if (currentBrowserProcess) { - currentBrowserProcess.close() + await currentBrowserProcess.close() currentBrowserProcess = null } }) @@ -415,8 +420,12 @@ ipcMain.handle('har:open', async (_, fileName: string): Promise => { try { fileHandle = await open(path.join(RECORDINGS_PATH, fileName), 'r') const data = await fileHandle?.readFile({ encoding: 'utf-8' }) + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const har = await JSON.parse(data) + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment return { name: fileName, content: har } } finally { await fileHandle?.close() @@ -470,8 +479,12 @@ ipcMain.handle( fileHandle = await open(path.join(GENERATORS_PATH, fileName), 'r') const data = await fileHandle?.readFile({ encoding: 'utf-8' }) + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const generator = await JSON.parse(data) + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment return { name: fileName, content: generator } } finally { await fileHandle?.close() @@ -490,7 +503,7 @@ ipcMain.handle('ui:delete-file', async (_, fileName: string) => { return unlink(getFilePathFromName(fileName)) }) -ipcMain.on('ui:open-folder', async (_, fileName: string) => { +ipcMain.on('ui:open-folder', (_, fileName: string) => { console.info('ui:open-folder event received') shell.showItemInFolder(getFilePathFromName(fileName)) }) @@ -559,10 +572,10 @@ ipcMain.on('splashscreen:close', (event) => { ipcMain.handle('browser:open:external:link', (_, url: string) => { console.info('browser:open:external:link event received') - shell.openExternal(url) + return shell.openExternal(url) }) -ipcMain.handle('log:open', () => { +ipcMain.on('log:open', () => { console.info('log:open event received') openLogFolder() }) @@ -585,7 +598,7 @@ ipcMain.handle('settings:save', async (event, data: AppSettings) => { // don't pass fields that are not submitted by the form const { windowState: _, ...settings } = data const modifiedSettings = await saveSettings(settings) - applySettings(modifiedSettings, browserWindow) + await applySettings(modifiedSettings, browserWindow) sendToast(browserWindow.webContents, { title: 'Settings saved successfully', @@ -610,7 +623,7 @@ ipcMain.handle('settings:select-upstream-certificate', async () => { return selectUpstreamCertificate() }) -ipcMain.handle('proxy:status:get', async () => { +ipcMain.handle('proxy:status:get', () => { console.info('proxy:status:get event received') return proxyStatus }) @@ -706,7 +719,7 @@ function showWindow(browserWindow: BrowserWindow) { browserWindow.focus() } -function trackWindowState(browserWindow: BrowserWindow) { +async function trackWindowState(browserWindow: BrowserWindow) { const { width, height, x, y } = browserWindow.getBounds() const isMaximized = browserWindow.isMaximized() appSettings.windowState = { @@ -717,7 +730,7 @@ function trackWindowState(browserWindow: BrowserWindow) { isMaximized, } try { - saveSettings(appSettings) + await saveSettings(appSettings) } catch (error) { log.error(error) } diff --git a/src/preload.ts b/src/preload.ts index c2485c6c..be844c84 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,3 +1,5 @@ +// TODO: https://github.com/grafana/k6-studio/issues/277 +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron' import { ProxyData, K6Log, K6Check, ProxyStatus } from './types' import { HarFile } from './types/har' @@ -34,7 +36,7 @@ const proxy = { onProxyData: (callback: (data: ProxyData) => void) => { return createListener('proxy:data', callback) }, - getProxyStatus: () => { + getProxyStatus: (): Promise => { return ipcRenderer.invoke('proxy:status:get') }, onProxyStatusChange: (callback: (status: ProxyStatus) => void) => { @@ -159,16 +161,16 @@ const app = { return createListener('app:close', callback) }, closeApplication: () => { - ipcRenderer.invoke('app:close') + ipcRenderer.send('app:close') }, changeRoute: (route: string) => { - return ipcRenderer.invoke('app:change-route', route) + return ipcRenderer.send('app:change-route', route) }, } as const const log = { openLogFolder: () => { - ipcRenderer.invoke('log:open') + ipcRenderer.send('log:open') }, getLogContent: (): Promise => { return ipcRenderer.invoke('log:read') diff --git a/src/proxy.ts b/src/proxy.ts index 92fe33f8..40ddc99a 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -94,7 +94,7 @@ export const launchProxy = ( }) proxy.stderr.on('data', (data: Buffer) => { - console.error(`stderr: ${data}`) + console.error(`stderr: ${data.toString()}`) log.error(data.toString()) }) diff --git a/src/rules/correlation.ts b/src/rules/correlation.ts index e4e116a7..2e946418 100644 --- a/src/rules/correlation.ts +++ b/src/rules/correlation.ts @@ -61,7 +61,7 @@ function applyRule({ requestSnippetSchema: RequestSnippetSchema state: CorrelationState rule: CorrelationRule - idGenerator: Generator + idGenerator: IdGenerator setState: (newState: Partial) => void }) { // this is the modified schema that we return to the accumulator @@ -108,6 +108,8 @@ function applyRule({ } // try to extract the value + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { extractedValue, generatedUniqueId, correlationExtractionSnippet } = tryCorrelationExtraction( rule, @@ -126,7 +128,9 @@ function applyRule({ } setState({ - extractedValue: extractedValue, + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + extractedValue, generatedUniqueId: generatedUniqueId, responsesExtracted: [ ...state.responsesExtracted, @@ -542,9 +546,14 @@ const extractCorrelationJsonBody = ( return noCorrelationResult } + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const extractedValue = getJsonObjectFromPath(response.content, selector.path) - if (!extractedValue || extractedValue.length === 0) { + if ( + !extractedValue || + (Array.isArray(extractedValue) && extractedValue.length === 0) + ) { return noCorrelationResult } @@ -558,6 +567,8 @@ const extractCorrelationJsonBody = ( const correlationExtractionSnippet = ` correlation_vars['correlation_${generatedUniqueId}'] = resp.json()${json_path}` return { + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment extractedValue, correlationExtractionSnippet, generatedUniqueId, diff --git a/src/rules/parameterization.ts b/src/rules/parameterization.ts index acdad2e7..fb6f8d6d 100644 --- a/src/rules/parameterization.ts +++ b/src/rules/parameterization.ts @@ -10,7 +10,7 @@ import { matchFilter } from './utils' export function createParameterizationRuleInstance( rule: ParameterizationRule, - idGenerator: Generator + idGenerator: Generator ): ParameterizationRuleInstance { const state: ParameterizationState = { requestsReplaced: [], diff --git a/src/rules/rules.ts b/src/rules/rules.ts index aeebf0ea..b8b8ddda 100644 --- a/src/rules/rules.ts +++ b/src/rules/rules.ts @@ -8,7 +8,10 @@ import { ProxyData, RequestSnippetSchema } from '@/types' import { generateSequentialInt } from './utils' function createSequentialIdPool() { - const currentId: Record> = { + const currentId: Record< + TestRule['type'], + Generator + > = { correlation: generateSequentialInt(), parameterization: generateSequentialInt(), verification: generateSequentialInt(), @@ -37,7 +40,7 @@ export function applyRules(recording: ProxyData[], rules: TestRule[]) { function createRuleInstance( rule: T, - idGenerator: Generator + idGenerator: Generator ) { switch (rule.type) { case 'correlation': diff --git a/src/rules/shared.ts b/src/rules/shared.ts index 326a1bd0..c196cd81 100644 --- a/src/rules/shared.ts +++ b/src/rules/shared.ts @@ -116,6 +116,8 @@ export const matchRegex = (value: string, regexString: string) => { } export const getJsonObjectFromPath = (json: string, path: string) => { + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return get(safeJsonParse(json), path) } @@ -272,6 +274,8 @@ export const replaceJsonBody = ( // since we are using lodash and its `set` function creates missing paths we will first check that the path really // exists before setting it + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const valueToReplace = getJsonObjectFromPath(request.content, selector.path) if (!valueToReplace) return diff --git a/src/script.ts b/src/script.ts index 213a4f40..c0b240c9 100644 --- a/src/script.ts +++ b/src/script.ts @@ -83,11 +83,15 @@ export const runScript = async ( stdoutReader.on('line', (data) => { console.log(`stdout: ${data}`) + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const checkData: K6Check[] = JSON.parse(data) browserWindow.webContents.send('script:check', checkData) }) stderrReader.on('line', (data) => { + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const logData: K6Log = JSON.parse(data) browserWindow.webContents.send('script:log', logData) }) diff --git a/src/settings.ts b/src/settings.ts index d2983f72..1c1722df 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -55,7 +55,11 @@ export async function getSettings() { const fileHandle = await open(filePath, 'r') try { const settings = await fileHandle?.readFile({ encoding: 'utf-8' }) + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const currentSettings = JSON.parse(settings) + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const allSettings = { ...defaultSettings, ...currentSettings, diff --git a/src/utils/format.ts b/src/utils/format.ts index 51d742b9..eb9e340c 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -1,7 +1,11 @@ export function queryStringToJSON(str: string) { + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return JSON.parse( '{"' + str.replace(/&/g, '","').replace(/=/g, '":"') + '"}', function (key, value) { + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument return key === '' ? value : decodeURIComponent(value) } ) diff --git a/src/utils/harToProxyData.ts b/src/utils/harToProxyData.ts index 884f8123..87297477 100644 --- a/src/utils/harToProxyData.ts +++ b/src/utils/harToProxyData.ts @@ -41,7 +41,9 @@ function parseRequest(request: Entry['request']): Request { // TODO: add actual values // @ts-expect-error incomplete type timestampStart: request.startedDateTime - ? // @ts-expect-error incomplete type + ? // TODO: https://github.com/grafana/k6-studio/issues/277 + // @ts-expect-error incomplete type + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument isoToUnixTimestamp(request.startedDateTime) : 0, timestampEnd: 0, diff --git a/src/views/Generator/ExportScriptDialog/OverwriteFileWarning.tsx b/src/views/Generator/ExportScriptDialog/OverwriteFileWarning.tsx index 1dce607f..b7ee89ce 100644 --- a/src/views/Generator/ExportScriptDialog/OverwriteFileWarning.tsx +++ b/src/views/Generator/ExportScriptDialog/OverwriteFileWarning.tsx @@ -9,6 +9,8 @@ export function OverwriteFileWarning() { setValue('overwriteFile', false) } + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const scriptName = getScriptNameWithExtension(getValues('scriptName')) return ( diff --git a/src/views/Generator/Generator.hooks.ts b/src/views/Generator/Generator.hooks.ts index af6d1e1f..448f5038 100644 --- a/src/views/Generator/Generator.hooks.ts +++ b/src/views/Generator/Generator.hooks.ts @@ -52,7 +52,7 @@ export function useSaveGeneratorFile(fileName: string) { return useMutation({ mutationFn: async (generator: GeneratorFileData) => { await writeGeneratorToFile(fileName, generator) - queryClient.invalidateQueries({ queryKey: ['generator', fileName] }) + await queryClient.invalidateQueries({ queryKey: ['generator', fileName] }) }, onSuccess: () => { diff --git a/src/views/Generator/Generator.tsx b/src/views/Generator/Generator.tsx index e7108b5a..85dfd4ab 100644 --- a/src/views/Generator/Generator.tsx +++ b/src/views/Generator/Generator.tsx @@ -58,6 +58,8 @@ export function Generator() { const blocker = useBlocker(({ historyAction }) => { // Don't block navigation when redirecting home from invalid generator + // TODO(router): Action enum is not exported from react-router-dom + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison return isDirty && historyAction !== 'REPLACE' }) diff --git a/src/views/Generator/Generator.utils.ts b/src/views/Generator/Generator.utils.ts index 7a92cae1..412a5ad8 100644 --- a/src/views/Generator/Generator.utils.ts +++ b/src/views/Generator/Generator.utils.ts @@ -22,17 +22,13 @@ export async function generateScriptPreview( return prettify(script) } -export function saveScript(script: string, fileName: string) { - window.studio.script.saveScript(script, fileName) -} - export async function exportScript(fileName: string) { const generator = selectGeneratorData(useGeneratorStore.getState()) const filteredRequests = selectFilteredRequests(useGeneratorStore.getState()) const script = await generateScriptPreview(generator, filteredRequests) - saveScript(script, fileName) + await window.studio.script.saveScript(script, fileName) } export const scriptExists = async (fileName: string) => { diff --git a/src/views/Generator/RuleEditor/CustomCodeEditor.tsx b/src/views/Generator/RuleEditor/CustomCodeEditor.tsx index 91c5c48f..5e545138 100644 --- a/src/views/Generator/RuleEditor/CustomCodeEditor.tsx +++ b/src/views/Generator/RuleEditor/CustomCodeEditor.tsx @@ -34,6 +34,8 @@ export function CustomCodeEditor() { render={({ field }) => (
{ field.onChange(value) diff --git a/src/views/Generator/ValidatorDialog.tsx b/src/views/Generator/ValidatorDialog.tsx index 7ab0c1bf..3aedd1b4 100644 --- a/src/views/Generator/ValidatorDialog.tsx +++ b/src/views/Generator/ValidatorDialog.tsx @@ -58,6 +58,8 @@ export function ValidatorDialog({ useEffect(() => { if (!open) return + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-floating-promises handleRunScript() }, [open, handleRunScript]) diff --git a/src/views/Recorder/Recorder.tsx b/src/views/Recorder/Recorder.tsx index 6a6b93c1..85a2a043 100644 --- a/src/views/Recorder/Recorder.tsx +++ b/src/views/Recorder/Recorder.tsx @@ -117,7 +117,7 @@ export function Recorder() { } }, [groups, proxyData, startUrl]) - async function handleStopRecording() { + function handleStopRecording() { stopRecording() } diff --git a/src/views/RecordingPreviewer/RecordingPreviewer.tsx b/src/views/RecordingPreviewer/RecordingPreviewer.tsx index 1993d048..a37d71e0 100644 --- a/src/views/RecordingPreviewer/RecordingPreviewer.tsx +++ b/src/views/RecordingPreviewer/RecordingPreviewer.tsx @@ -25,7 +25,11 @@ export function RecordingPreviewer() { const [selectedRequest, setSelectedRequest] = useState(null) const { fileName } = useParams() const navigate = useNavigate() + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { state } = useLocation() + // TODO: https://github.com/grafana/k6-studio/issues/277 + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const isDiscardable = Boolean(state?.discardable) invariant(fileName, 'fileName is required') diff --git a/src/views/Validator/Validator.hooks.ts b/src/views/Validator/Validator.hooks.ts index fe9c861b..0c2f7e58 100644 --- a/src/views/Validator/Validator.hooks.ts +++ b/src/views/Validator/Validator.hooks.ts @@ -2,7 +2,8 @@ import { useLocation, useParams } from 'react-router-dom' export function useScriptPath() { const { fileName } = useParams() - const { state } = useLocation() + // TODO(router): useLocation is not type-safe. Refactor this route to avoid using it. + const { state } = useLocation() as { state: { externalScriptPath: string } } return { scriptPath: fileName || state?.externalScriptPath, diff --git a/src/views/Validator/Validator.tsx b/src/views/Validator/Validator.tsx index 85e614f1..2085ae4c 100644 --- a/src/views/Validator/Validator.tsx +++ b/src/views/Validator/Validator.tsx @@ -60,7 +60,7 @@ export function Validator() { navigate(getRoutePath('home')) } - function handleRunScript() { + async function handleRunScript() { if (!scriptPath) { return } @@ -68,8 +68,8 @@ export function Validator() { resetProxyData() resetLogs() resetChecks() - window.studio.script.runScript(scriptPath, isExternal) setIsRunning(true) + await window.studio.script.runScript(scriptPath, isExternal) } function handleStopScript() {