Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Simpler default file names #453

Merged
merged 5 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Layout/Sidebar/Sidebar.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { orderBy } from 'lodash-es'
import { useEffect, useMemo } from 'react'

function orderByFileName(files: Map<string, StudioFile>) {
return orderBy([...files.values()], (s) => s.fileName)
return orderBy([...files.values()], (s) => s.displayName)
}

function toFileMap(files: StudioFile[]) {
Expand Down
5 changes: 3 additions & 2 deletions src/components/Layout/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ interface SidebarProps {
export function Sidebar({ isExpanded, onCollapseSidebar }: SidebarProps) {
const [searchTerm, setSearchTerm] = useState('')
const { recordings, generators, scripts, dataFiles } = useFiles(searchTerm)

const createNewGenerator = useCreateGenerator()

const handleCreateNewGenerator = () => createNewGenerator()

const handleImportDataFile = () => {
return window.studio.data.importFile()
}
Expand Down Expand Up @@ -92,7 +93,7 @@ export function Sidebar({ isExpanded, onCollapseSidebar }: SidebarProps) {
aria-label="New generator"
variant="ghost"
size="1"
onClick={createNewGenerator}
onClick={handleCreateNewGenerator}
css={{ cursor: 'pointer' }}
>
<PlusIcon />
Expand Down
25 changes: 3 additions & 22 deletions src/hooks/useCreateGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { useNavigate } from 'react-router-dom'
import { useCreateGenerator } from './useCreateGenerator'
import { createNewGeneratorFile } from '@/utils/generator'
import { generateFileNameWithTimestamp } from '@/utils/file'
import { getRoutePath } from '@/routeMap'
import { useToast } from '@/store/ui/useToast'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderHook } from '@testing-library/react'
import { act } from 'react'
import { createGeneratorData } from '@/test/factories/generator'

vi.mock('react-router-dom', () => ({
useNavigate: vi.fn(),
}))
vi.mock('@/utils/generator', () => ({
createNewGeneratorFile: vi.fn(),
}))
vi.mock('@/utils/file', () => ({
generateFileNameWithTimestamp: vi.fn(),
}))
vi.mock('@/routeMap', () => ({
getRoutePath: vi.fn(),
}))
Expand All @@ -41,16 +32,14 @@ describe('useCreateGenerator', () => {
})

it('should navigate to the correct path on successful generator creation', async () => {
const newGenerator = createGeneratorData()
const fileName = 'test-file.json'
const routePath = '/generator/test-file.json'

vi.mocked(createNewGeneratorFile).mockReturnValue(newGenerator)
vi.mocked(generateFileNameWithTimestamp).mockReturnValue(fileName)
vi.mocked(getRoutePath).mockReturnValue(routePath)
vi.stubGlobal('studio', {
generator: {
saveGenerator: vi.fn().mockResolvedValue(fileName),
createGenerator: vi.fn().mockResolvedValue(fileName),
saveGenerator: vi.fn(),
loadGenerator: vi.fn(),
},
})
Expand All @@ -61,15 +50,7 @@ describe('useCreateGenerator', () => {
await result.current()
})

expect(createNewGeneratorFile).toHaveBeenCalled()
expect(generateFileNameWithTimestamp).toHaveBeenCalledWith(
'json',
'Generator'
)
expect(window.studio.generator.saveGenerator).toHaveBeenCalledWith(
JSON.stringify(newGenerator, null, 2),
fileName
)
expect(window.studio.generator.createGenerator).toHaveBeenCalledWith('')
expect(navigate).toHaveBeenCalledWith(routePath)
})

Expand Down
38 changes: 18 additions & 20 deletions src/hooks/useCreateGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useCallback } from 'react'
import { useNavigate } from 'react-router-dom'

import { generateFileNameWithTimestamp } from '@/utils/file'
import { createNewGeneratorFile } from '@/utils/generator'
import { getRoutePath } from '@/routeMap'
import { useToast } from '@/store/ui/useToast'
import log from 'electron-log/renderer'
Expand All @@ -11,25 +9,25 @@ export function useCreateGenerator() {
const navigate = useNavigate()
const showToast = useToast()

const createTestGenerator = useCallback(async () => {
try {
const newGenerator = createNewGeneratorFile()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do it in the render process

const fileName = await window.studio.generator.saveGenerator(
JSON.stringify(newGenerator, null, 2),
generateFileNameWithTimestamp('json', 'Generator')
)
const createTestGenerator = useCallback(
async (recordingPath = '') => {
try {
const fileName =
await window.studio.generator.createGenerator(recordingPath)

navigate(
getRoutePath('generator', { fileName: encodeURIComponent(fileName) })
)
} catch (error) {
showToast({
status: 'error',
title: 'Failed to create generator',
})
log.error(error)
}
}, [navigate, showToast])
navigate(
getRoutePath('generator', { fileName: encodeURIComponent(fileName) })
)
} catch (error) {
showToast({
status: 'error',
title: 'Failed to create generator',
})
log.error(error)
}
},
[navigate, showToast]
)

return createTestGenerator
}
101 changes: 77 additions & 24 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ import {
INVALID_FILENAME_CHARS,
TEMP_GENERATOR_SCRIPT_FILENAME,
} from './constants/files'
import { generateFileNameWithTimestamp } from './utils/file'
import { HarFile } from './types/har'
import { GeneratorFile } from './types/generator'
import { HarFile, HarWithOptionalResponse } from './types/har'
import { GeneratorFileData } from './types/generator'
import kill from 'tree-kill'
import find from 'find-process'
import { getLogContent, initializeLogger, openLogFolder } from './logger'
Expand All @@ -67,9 +66,10 @@ import {
import { ProxyStatus, StudioFile } from './types'
import { configureApplicationMenu } from './menu'
import * as Sentry from '@sentry/electron/main'
import { exhaustive } from './utils/typescript'
import { exhaustive, isNodeJsErrnoException } from './utils/typescript'
import { DataFilePreview } from './types/testData'
import { parseDataFile } from './utils/dataFile'
import { createNewGeneratorFile } from './utils/generator'

if (process.env.NODE_ENV !== 'development') {
// handle auto updates
Expand Down Expand Up @@ -427,11 +427,19 @@ ipcMain.handle(
)

// HAR
ipcMain.handle('har:save', async (_, data: string, prefix?: string) => {
const fileName = generateFileNameWithTimestamp('har', prefix)
await writeFile(path.join(RECORDINGS_PATH, fileName), data)
return fileName
})
ipcMain.handle(
'har:save',
async (_, data: HarWithOptionalResponse, prefix: string) => {
const fileName = await createFileWithUniqueName({
data: JSON.stringify(data, null, 2),
directory: RECORDINGS_PATH,
ext: '.har',
prefix,
})

return fileName
}
)

ipcMain.handle('har:open', async (_, fileName: string): Promise<HarFile> => {
console.info('har:open event received')
Expand Down Expand Up @@ -475,23 +483,33 @@ ipcMain.handle('har:import', async (event) => {
})

// Generator
ipcMain.handle('generator:create', async (_, recordingPath: string) => {
const generator = createNewGeneratorFile(recordingPath)
const fileName = await createFileWithUniqueName({
data: JSON.stringify(generator, null, 2),
directory: GENERATORS_PATH,
ext: '.json',
prefix: 'Generator',
})

return fileName
})

ipcMain.handle(
'generator:save',
async (_, generatorFile: string, fileName: string) => {
console.info('generator:save event received')

async (_, generator: GeneratorFileData, fileName: string) => {
invariant(!INVALID_FILENAME_CHARS.test(fileName), 'Invalid file name')

await writeFile(path.join(GENERATORS_PATH, fileName), generatorFile)
return fileName
await writeFile(
path.join(GENERATORS_PATH, fileName),
JSON.stringify(generator, null, 2)
)
}
)

ipcMain.handle(
'generator:open',
async (_, fileName: string): Promise<GeneratorFile> => {
console.info('generator:open event received')

async (_, fileName: string): Promise<GeneratorFileData> => {
let fileHandle: FileHandle | undefined

try {
Expand All @@ -502,9 +520,7 @@ ipcMain.handle(
// 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 }
return generator
} finally {
await fileHandle?.close()
}
Expand Down Expand Up @@ -589,10 +605,7 @@ ipcMain.handle(
await access(newPath)
throw new Error(`File with name ${newFileName} already exists`)
} catch (error) {
if (
error instanceof Error &&
(error as NodeJS.ErrnoException).code !== 'ENOENT'
) {
if (isNodeJsErrnoException(error) && error.code !== 'ENOENT') {
throw error
}
}
Expand Down Expand Up @@ -962,3 +975,43 @@ const cleanUpProxies = async () => {
kill(proc.pid)
})
}

const createFileWithUniqueName = async ({
directory,
data,
prefix,
ext,
}: {
directory: string
data: string
prefix: string
ext: string
}): Promise<string> => {
const timestamp = new Date().toISOString().split('T')[0] ?? ''
const template = `${prefix ? `${prefix} - ` : ''}${timestamp}${ext}`

// Start from 2 as it follows the the OS behavior for duplicate files
let fileVersion = 2
let uniqueFileName = template
let fileCreated = false

do {
try {
// ax+ flag will throw an error if the file already exists
await writeFile(path.join(directory, uniqueFileName), data, {
flag: 'ax+',
})
fileCreated = true
} catch (error) {
if (isNodeJsErrnoException(error) && error.code !== 'EEXIST') {
throw error
}

const { name, ext } = path.parse(template)
uniqueFileName = `${name} (${fileVersion})${ext}`
fileVersion++
}
} while (!fileCreated)

return uniqueFileName
}
21 changes: 15 additions & 6 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ipcRenderer, contextBridge, IpcRendererEvent } from 'electron'
import { ProxyData, K6Log, K6Check, ProxyStatus, StudioFile } from './types'
import { HarFile } from './types/har'
import { GeneratorFile } from './types/generator'
import { HarFile, HarWithOptionalResponse } from './types/har'
import { GeneratorFileData } from './types/generator'
import { AddToastPayload } from './types/toast'
import { AppSettings } from './types/settings'
import * as Sentry from './sentry'
Expand Down Expand Up @@ -107,7 +107,10 @@ const script = {
} as const

const har = {
saveFile: (data: string, prefix?: string): Promise<string> => {
saveFile: (
data: HarWithOptionalResponse,
prefix: string
): Promise<string> => {
return ipcRenderer.invoke('har:save', data, prefix)
},
openFile: (filePath: string): Promise<HarFile> => {
Expand Down Expand Up @@ -160,10 +163,16 @@ const ui = {
} as const

const generator = {
saveGenerator: (generatorFile: string, fileName: string): Promise<string> => {
return ipcRenderer.invoke('generator:save', generatorFile, fileName)
createGenerator: (recordingPath: string): Promise<string> => {
return ipcRenderer.invoke('generator:create', recordingPath)
},
saveGenerator: (
generator: GeneratorFileData,
fileName: string
): Promise<void> => {
return ipcRenderer.invoke('generator:save', generator, fileName)
},
loadGenerator: (fileName: string): Promise<GeneratorFile> => {
loadGenerator: (fileName: string): Promise<GeneratorFileData> => {
return ipcRenderer.invoke('generator:open', fileName)
},
} as const
Expand Down
5 changes: 0 additions & 5 deletions src/types/generator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import { z } from 'zod'
import { GeneratorFileDataSchema } from '@/schemas/generator'

export interface GeneratorFile {
name: string
content: GeneratorFileData
}

export type GeneratorFileData = z.infer<typeof GeneratorFileDataSchema>
13 changes: 0 additions & 13 deletions src/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,6 @@ import { getRoutePath } from '../routeMap'
import { StudioFileType } from '@/types'
import { exhaustive } from './typescript'

export function generateFileNameWithTimestamp(
extension: string,
prefix?: string
) {
const timestamp =
new Date()
.toISOString()
.replace(/:/g, '-')
.replace(/T/g, '_')
.split('.')[0] ?? ''
return `${prefix ? `${prefix} - ` : ''}${timestamp}.${extension}`
}

export function getFileNameWithoutExtension(fileName: string) {
return fileName.replace(/\.[^/.]+$/, '')
}
Expand Down
Loading