-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from 2 commits
d9873c8
e883209
c2153db
63331f5
a509aed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -70,6 +69,7 @@ import * as Sentry from '@sentry/electron/main' | |
import { exhaustive } 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 | ||
|
@@ -427,11 +427,22 @@ 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 generateUniqueFileName({ | ||
directory: RECORDINGS_PATH, | ||
ext: '.har', | ||
prefix, | ||
}) | ||
|
||
await writeFile( | ||
path.join(RECORDINGS_PATH, fileName), | ||
JSON.stringify(data, null, 4) | ||
going-confetti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
return fileName | ||
} | ||
) | ||
|
||
ipcMain.handle('har:open', async (_, fileName: string): Promise<HarFile> => { | ||
console.info('har:open event received') | ||
|
@@ -475,23 +486,37 @@ ipcMain.handle('har:import', async (event) => { | |
}) | ||
|
||
// Generator | ||
ipcMain.handle('generator:create', async (_, recordingPath: string) => { | ||
const generator = createNewGeneratorFile(recordingPath) | ||
const fileName = await generateUniqueFileName({ | ||
directory: GENERATORS_PATH, | ||
ext: '.json', | ||
prefix: 'Generator', | ||
}) | ||
|
||
await writeFile( | ||
path.join(GENERATORS_PATH, fileName), | ||
JSON.stringify(generator, null, 2) | ||
) | ||
|
||
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 { | ||
|
@@ -502,9 +527,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() | ||
} | ||
|
@@ -962,3 +985,35 @@ const cleanUpProxies = async () => { | |
kill(proc.pid) | ||
}) | ||
} | ||
|
||
const generateUniqueFileName = async ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be in a util file, but I'd need to create a new one, since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is where having the app split up based on it's entry point would be handy:
This would allows us to setup a |
||
directory, | ||
prefix, | ||
ext, | ||
}: { | ||
directory: string | ||
prefix: string | ||
ext: string | ||
}): Promise<string> => { | ||
const timestamp = new Date().toISOString().split('T')[0] ?? '' | ||
const template = `${prefix ? `${prefix} - ` : ''}${timestamp}${ext}` | ||
|
||
let uniqueFileName = template | ||
let fileExists = await access(path.join(directory, uniqueFileName)) | ||
.then(() => true) | ||
.catch(() => false) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason why we shouldn't use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using async fs functions follows the electron performance recommendations and Interestingly, there's the following statement about
Perhaps, the pattern itself is wrong, and even There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think what you want to do is use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO it's okay to use these methods in certain places (like a one-off operation, during initialization) that don't compromise the event loop. @allansson's suggestion seems to handle both the check and the write at the same time so we can consider that approach as well. If using const fileExists = await checkFileExsits(path.join(directory, uniqueFileName) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I rewrote the function to use |
||
|
||
// Start from 2 as it follows the the OS behavior for duplicate files | ||
let counter = 2 | ||
|
||
while (fileExists) { | ||
const { name, ext } = path.parse(template) | ||
uniqueFileName = `${name} (${counter})${ext}` | ||
fileExists = await access(path.join(directory, uniqueFileName)) | ||
.then(() => true) | ||
.catch(() => false) | ||
counter++ | ||
} | ||
|
||
return uniqueFileName | ||
} |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ export function createEmptyRule(type: TestRule['type']): TestRule { | |
case 'correlation': | ||
return { | ||
type: 'correlation', | ||
id: self.crypto.randomUUID(), | ||
id: crypto.randomUUID(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: although it's not a big deal, should we stop storing rule ids in the generator file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now, I believe it's only used by the state manager (so we know each one is selected/being edited etc). It might be easier to have an id on file than setting than during runtime 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should store them because:
Like @cristianoventura said, setting them during runtime is harder. You'd have to traverse the entire hierarchy of objects and slap and id on the things you care about, and if you load the thing from disk again you'll get an entirely new object with new id:s. For instance, consider checking if a file has unsaved changes. If the id:s aren't stored, we would have to strip all the ids from everything before doing any comparison. That said, could we switch to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We could, but what would be the benefits of this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
None, really. A bit more efficient (crypto is computationally expensive), saves a few bytes of space and IMO they are easier to read than UUIDs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the last argument is the strongest one, but these ids aren't user-facing so I wouldn't make it a priority. |
||
enabled: true, | ||
extractor: { | ||
filter: { path: '' }, | ||
|
@@ -23,7 +23,7 @@ export function createEmptyRule(type: TestRule['type']): TestRule { | |
case 'customCode': | ||
return { | ||
type: 'customCode', | ||
id: self.crypto.randomUUID(), | ||
id: crypto.randomUUID(), | ||
enabled: true, | ||
filter: { path: '' }, | ||
snippet: '', | ||
|
@@ -32,7 +32,7 @@ export function createEmptyRule(type: TestRule['type']): TestRule { | |
case 'parameterization': | ||
return { | ||
type: 'parameterization', | ||
id: self.crypto.randomUUID(), | ||
id: crypto.randomUUID(), | ||
enabled: true, | ||
filter: { path: '' }, | ||
selector: { | ||
|
@@ -46,7 +46,7 @@ export function createEmptyRule(type: TestRule['type']): TestRule { | |
case 'verification': | ||
return { | ||
type: 'verification', | ||
id: self.crypto.randomUUID(), | ||
id: crypto.randomUUID(), | ||
enabled: true, | ||
filter: { path: '' }, | ||
selector: { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,11 +2,7 @@ import { useParams } from 'react-router-dom' | |
import invariant from 'tiny-invariant' | ||
|
||
import { useToast } from '@/store/ui/useToast' | ||
import { | ||
loadGeneratorFile, | ||
loadHarFile, | ||
writeGeneratorToFile, | ||
} from './Generator.utils' | ||
import { loadGeneratorFile, loadHarFile } from './Generator.utils' | ||
import { selectGeneratorData, useGeneratorStore } from '@/store/generator' | ||
import { GeneratorFileData } from '@/types/generator' | ||
import { useMutation, useQuery } from '@tanstack/react-query' | ||
|
@@ -41,7 +37,10 @@ export function useUpdateValueInGeneratorFile(fileName: string) { | |
return useMutation({ | ||
mutationFn: async ({ key, value }: { key: string; value: unknown }) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing to do with this PR really, but just an idea: instead of passing a key and value to update (which doesn't seem to be type safe?), why not use a useUpdateInFile(fileName: (generator: Generator) => {
return {
...generator,
myValue: "hello"
}
}) |
||
const generator = await loadGeneratorFile(fileName) | ||
await writeGeneratorToFile(fileName, { ...generator, [key]: value }) | ||
await window.studio.generator.saveGenerator( | ||
{ ...generator, [key]: value }, | ||
fileName | ||
) | ||
}, | ||
}) | ||
} | ||
|
@@ -51,7 +50,7 @@ export function useSaveGeneratorFile(fileName: string) { | |
|
||
return useMutation({ | ||
mutationFn: async (generator: GeneratorFileData) => { | ||
await writeGeneratorToFile(fileName, generator) | ||
await window.studio.generator.saveGenerator(generator, fileName) | ||
await queryClient.invalidateQueries({ queryKey: ['generator', fileName] }) | ||
}, | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,19 +31,9 @@ export async function exportScript(fileName: string) { | |
await window.studio.script.saveScript(script, fileName) | ||
} | ||
|
||
export const writeGeneratorToFile = ( | ||
fileName: string, | ||
generatorData: GeneratorFileData | ||
) => { | ||
return window.studio.generator.saveGenerator( | ||
JSON.stringify(generatorData, null, 2), | ||
fileName | ||
) | ||
} | ||
|
||
export const loadGeneratorFile = async (fileName: string) => { | ||
const generatorFile = await window.studio.generator.loadGenerator(fileName) | ||
return GeneratorFileDataSchema.parse(generatorFile.content) | ||
const generator = await window.studio.generator.loadGenerator(fileName) | ||
return GeneratorFileDataSchema.parse(generator) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO this should be happening in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll open a separate PR for this change |
||
} | ||
|
||
export const loadHarFile = async (fileName: string) => { | ||
|
There was a problem hiding this comment.
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