-
Notifications
You must be signed in to change notification settings - Fork 0
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: persist proxy preferences #254
Conversation
let proxyStatus: ProxyStatus = 'offline' | ||
export let appSettings: AppSettings |
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.
- Proxy status will be tracked across its life cycle so we can observe it in the UI.
- All the app configuration now lives in
appSettings
. This variable is dynamic and will change as settings are changed.
proxyEmitter.on('status:change', (statusName: ProxyStatus) => { | ||
proxyStatus = statusName | ||
mainWindow.webContents.send('proxy:status:change', statusName) | ||
}) |
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.
Since we already have an EventEmitter for the proxy I used it as a wrapper for changes related to the proxy status.
|
||
return mainWindow | ||
} | ||
|
||
app.whenReady().then(async () => { | ||
appSettings = await getSettings() |
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.
Retrieving settings is the first event when the app launches. getSettings
is an upsert so it'll create a configuration file if none is found.
ipcMain.handle('settings:get', async () => { | ||
console.info('settings:get event received') | ||
return await getSettings() | ||
}) |
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.
Event handler to retrieve settings in the UI.
src/main.ts
Outdated
ipcMain.handle('settings:save', async (event, data: AppSettings) => { | ||
console.info('settings:save event received') | ||
|
||
const browserWindow = browserWindowFromEvent(event) | ||
try { | ||
const diff = await saveSettings(data) | ||
applySettings(diff, browserWindow) |
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.
Event handler for saving settings from the UI. saveSettings
returns the diff with only settings that have been modified. applySettings
is responsible for overwriting and performing specific changes.
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.
minor nit: might make more sense to call it diff
-> modifiedSettings
🤔
'--listen-port', | ||
`${proxySettings.port}`, |
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.
The proxy should always listen on the port from the settings file.
src/proxy.ts
Outdated
if (proxyRequiresAuth(proxySettings)) { | ||
const { username, password } = proxySettings.upstream | ||
proxyArgs.push('--upstream-auth', `${username}:${password}`) | ||
} |
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.
Appends authentication argument when upstream mode requires auth.
src/main.ts
Outdated
async function applySettings( | ||
diff: Partial<AppSettings>, | ||
browserWindow: BrowserWindow | ||
) { | ||
if (diff.proxy) { |
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.
This is where configuration changes when settings are saved.
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.
same as above, renaming diff might make it more explicit/readable!
src/views/Settings/ProxySettings.tsx
Outdated
<Flex gap="2" mt="5"> | ||
<Text size="2"> | ||
Proxy status: <ProxyStatusIndicator status={proxyStatus} /> | ||
</Text> | ||
</Flex> |
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.
This proxy status can be moved to the footer/activity bar once we implement it.
src/settings.ts
Outdated
const defaultSettings: AppSettings = { | ||
appVersion: version, |
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.
This file contains helpers for handling the settings JSON, including the default configuration that will be set when no file is found.
Currently, we don't support any type of migration that allows the settings file to be backward compatible in case we introduce breaking changes. I'm including appVersion
here so we can keep track of the schema between releases.
src/schemas/appSettings.ts
Outdated
}) | ||
|
||
export const AppSettingsSchema = z.object({ | ||
appVersion: z.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.
Should this be schemaVersion
instead? I presume breaking changes will be rather rare, so we can bump the schema version when needed + add migration logic
src/schemas/appSettings.ts
Outdated
.object({ | ||
mode: z.enum(['regular', 'upstream']), | ||
port: z | ||
.number({ message: 'Port number is required' }) | ||
.int() | ||
.min(1) | ||
.max(65535, { message: 'Port number must be between 1 and 65535' }), | ||
findPort: z.boolean(), | ||
upstream: z.object({ | ||
url: z.string().url({ message: 'Invalid URL' }).or(z.literal('')), | ||
requireAuth: z.boolean(), | ||
username: z.string().optional(), | ||
password: z.string().optional(), | ||
}), | ||
}) |
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.
Not sure about storing the upstream URL in settings if the mode is set to `regular'. Would a discriminated union work better here?
TS example:
interface RegularModeSettings {
mode: 'regular'
port: number
findPort: boolean
}
interface UpstreamModeSettings {
mode: 'upstream'
port: number
findPort: boolean
url: string
requireAuth: boolean
username: string
password: string
}
type ProxySettings = RegularModeSettings | UpstreamModeSettings
There
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.
The discriminated union doesn't seem compatible with a ZodEffect
(the result of a refinement) 😢 Also, IMO it would be better if we preserved the structure of the schema regardless of the values.
As a user, I think it would be nice if the form remembered my previous configuration as well in case I switch back from regular
to upstream
in the future.
What are your thoughts?
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.
I don't think we plan on making app settings shareable, so it's probably not too important, but I think of a setting file as a representation of the current app settings, so having stuff that doesn't affect the app there seems odd to me.
Remembering previous choices may or may not be nice, depending on the setting IMO. For example, if I switch between two different configs often, then it makes a lot of sense, but if it's a setting I change once a year or so, then I'd find it confusing.
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.
For example, if I switch between two different configs often, then it makes a lot of sense
Although in this case, maybe the UX would need to be different: let the user create multiple proxy configurations and add a way to choose the active one in the recorder
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.
As a user, I think it would be nice if the form remembered my previous configuration as well in case I switch back from regular to upstream in the future.
I think it would make sense for the current session but outside of that what going-confetti mentioned seems like a better solution if it's something we want to add 🤔
The discriminated union doesn't seem compatible with a ZodEffect (the result of a refinement)
Do we have any alternative ? I also think that a discriminatedUnion would be perfect here to keep the file clean and concise 🤔
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.
I ended up getting a discriminatedUnion
to work. Now we're not persistent upstream configuration if proxy mode is regular. Same with the browser path when it's checked to detect it automatically!
src/schemas/appSettings.ts
Outdated
|
||
export const RecorderSettingsSchema = z | ||
.object({ | ||
detectBrowserPath: z.boolean(), |
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.
Similar to the other comment: if detectBrowserPath
is true
, there's no need to store browserPath
Co-authored-by: Uladzimir Dzmitračkoŭ <[email protected]>
src/main.ts
Outdated
ipcMain.handle('settings:save', async (event, data: AppSettings) => { | ||
console.info('settings:save event received') | ||
|
||
const browserWindow = browserWindowFromEvent(event) | ||
try { | ||
const diff = await saveSettings(data) | ||
applySettings(diff, browserWindow) |
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.
minor nit: might make more sense to call it diff
-> modifiedSettings
🤔
src/main.ts
Outdated
async function applySettings( | ||
diff: Partial<AppSettings>, | ||
browserWindow: BrowserWindow | ||
) { | ||
if (diff.proxy) { |
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.
same as above, renaming diff might make it more explicit/readable!
src/schemas/appSettings.ts
Outdated
findPort: z.boolean(), | ||
upstream: z.object({ | ||
url: z.string().url({ message: 'Invalid URL' }).or(z.literal('')), | ||
requireAuth: z.boolean(), |
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.
small nit:
findPort
-> automaticallyFindOpenPort
or automaticallyFindPort
(makes it more explicit what it does)
requireAuth
-> requiresAuth
(maybe ignore this one but it seemed like it made more sense since the "proxy" requires x)
src/schemas/appSettings.ts
Outdated
.object({ | ||
mode: z.enum(['regular', 'upstream']), | ||
port: z | ||
.number({ message: 'Port number is required' }) | ||
.int() | ||
.min(1) | ||
.max(65535, { message: 'Port number must be between 1 and 65535' }), | ||
findPort: z.boolean(), | ||
upstream: z.object({ | ||
url: z.string().url({ message: 'Invalid URL' }).or(z.literal('')), | ||
requireAuth: z.boolean(), | ||
username: z.string().optional(), | ||
password: z.string().optional(), | ||
}), | ||
}) |
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.
As a user, I think it would be nice if the form remembered my previous configuration as well in case I switch back from regular to upstream in the future.
I think it would make sense for the current session but outside of that what going-confetti mentioned seems like a better solution if it's something we want to add 🤔
The discriminated union doesn't seem compatible with a ZodEffect (the result of a refinement)
Do we have any alternative ? I also think that a discriminatedUnion would be perfect here to keep the file clean and concise 🤔
src/schemas/appSettings.ts
Outdated
export type AppSettings = z.infer<typeof AppSettingsSchema> | ||
export type ProxySettings = z.infer<typeof ProxySettingsSchema> |
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.
nit: for the other types we have created a src/types/filename.ts
specifically for types, maybe we should do the same here for consistency ? 🤔
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.
LGTM
@going-confetti @Llandy3d I made changes to the PR that include:
Screen.Recording.2024-10-17.at.1.28.55.PM.mov |
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.
🚀 🚀 🚀
export const RegularProxySettingsSchema = z.object({ | ||
mode: z.literal('regular'), | ||
port: z | ||
.number({ message: 'Port number is required' }) | ||
.int() | ||
.min(1) | ||
.max(65535, { message: 'Port number must be between 1 and 65535' }), | ||
automaticallyFindPort: z.boolean(), | ||
}) | ||
|
||
export const UpstreamProxySettingsSchema = RegularProxySettingsSchema.extend({ | ||
mode: z.literal('upstream'), | ||
url: z.string().url({ message: 'Invalid URL' }).or(z.literal('')), | ||
requiresAuth: z.boolean(), | ||
username: z.string().optional(), | ||
password: z.string().optional(), | ||
}) |
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.
small nit: I would have probably created a BaseProxySettingsSchema
to be shared between all the proxy modes and then have each of them extend that since defining the port is a common setting 🤔 (but it does not matter much here and can be a future refactor 🙇 )
Description
This PR adds a "Settings" page where users can set some preferences. The following was implemented:
Recorder
Proxy
port
is busy)How to Test
/Users/username/Library/Application Support/k6 Studio
)lsof -i tcp:6001
)Checklist
npm run lint
) and all checks pass.npm test
) and all tests pass.Screenshots (if appropriate):
Related PR(s)/Issue(s)
Resolves #236