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: persist proxy preferences #254

Merged
merged 31 commits into from
Oct 18, 2024
Merged

Conversation

cristianoventura
Copy link
Collaborator

Description

This PR adds a "Settings" page where users can set some preferences. The following was implemented:

Recorder

  • browser path (let users input an alternative path to where Chrome executable is located)

Proxy

  • mode (regular or upstream)
  • port
  • automatically find port (let k6 studio find an available port if port is busy)
  • upstream server and authentication

How to Test

  • Launch k6 studio and check that the settings JSON file was created in (/Users/username/Library/Application Support/k6 Studio)
  • Change the browser path and attempt to launch the recorder
  • Change the proxy port and check that a new process is running on that port (example: lsof -i tcp:6001)
  • Check that all changes are saved in the JSON file

Checklist

  • I have performed a self-review of my code.
  • I have added tests for my changes.
  • I have run linter locally (npm run lint) and all checks pass.
  • I have run tests locally (npm test) and all tests pass.
  • I have commented on my code, particularly in hard-to-understand areas.

Screenshots (if appropriate):

image

Related PR(s)/Issue(s)

Resolves #236

@cristianoventura cristianoventura self-assigned this Oct 15, 2024
Comment on lines +62 to +63
let proxyStatus: ProxyStatus = 'offline'
export let appSettings: AppSettings
Copy link
Collaborator Author

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.

Comment on lines +158 to +161
proxyEmitter.on('status:change', (statusName: ProxyStatus) => {
proxyStatus = statusName
mainWindow.webContents.send('proxy:status:change', statusName)
})
Copy link
Collaborator Author

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()
Copy link
Collaborator Author

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.

Comment on lines +520 to +523
ipcMain.handle('settings:get', async () => {
console.info('settings:get event received')
return await getSettings()
})
Copy link
Collaborator Author

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
Comment on lines 525 to 531
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)
Copy link
Collaborator Author

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.

Copy link
Member

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 🤔

Comment on lines +54 to +55
'--listen-port',
`${proxySettings.port}`,
Copy link
Collaborator Author

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
Comment on lines 60 to 63
if (proxyRequiresAuth(proxySettings)) {
const { username, password } = proxySettings.upstream
proxyArgs.push('--upstream-auth', `${username}:${password}`)
}
Copy link
Collaborator Author

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
Comment on lines 551 to 555
async function applySettings(
diff: Partial<AppSettings>,
browserWindow: BrowserWindow
) {
if (diff.proxy) {
Copy link
Collaborator Author

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.

Copy link
Member

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!

Comment on lines 93 to 97
<Flex gap="2" mt="5">
<Text size="2">
Proxy status: <ProxyStatusIndicator status={proxyStatus} />
</Text>
</Flex>
Copy link
Collaborator Author

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
Comment on lines 7 to 8
const defaultSettings: AppSettings = {
appVersion: version,
Copy link
Collaborator Author

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.

@cristianoventura cristianoventura marked this pull request as ready for review October 15, 2024 18:55
@cristianoventura cristianoventura requested a review from a team as a code owner October 15, 2024 18:55
@cristianoventura cristianoventura requested review from going-confetti and Llandy3d and removed request for a team October 15, 2024 18:55
src/components/Layout/ActivityBar/SettingsButton.tsx Outdated Show resolved Hide resolved
})

export const AppSettingsSchema = z.object({
appVersion: z.string(),
Copy link
Collaborator

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

Comment on lines 4 to 18
.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(),
}),
})
Copy link
Collaborator

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

Copy link
Collaborator Author

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?

Copy link
Collaborator

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.

Copy link
Collaborator

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

Copy link
Member

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 🤔

Copy link
Collaborator Author

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!


export const RecorderSettingsSchema = z
.object({
detectBrowserPath: z.boolean(),
Copy link
Collaborator

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

src/main.ts Outdated
Comment on lines 525 to 531
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)
Copy link
Member

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
Comment on lines 551 to 555
async function applySettings(
diff: Partial<AppSettings>,
browserWindow: BrowserWindow
) {
if (diff.proxy) {
Copy link
Member

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!

Comment on lines 11 to 14
findPort: z.boolean(),
upstream: z.object({
url: z.string().url({ message: 'Invalid URL' }).or(z.literal('')),
requireAuth: z.boolean(),
Copy link
Member

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)

Comment on lines 4 to 18
.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(),
}),
})
Copy link
Member

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 🤔

Comment on lines 70 to 71
export type AppSettings = z.infer<typeof AppSettingsSchema>
export type ProxySettings = z.infer<typeof ProxySettingsSchema>
Copy link
Member

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 ? 🤔

going-confetti
going-confetti previously approved these changes Oct 17, 2024
Copy link
Collaborator

@going-confetti going-confetti left a comment

Choose a reason for hiding this comment

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

LGTM

@cristianoventura
Copy link
Collaborator Author

@going-confetti @Llandy3d I made changes to the PR that include:

  • not storing upstream settings when mode is regular
  • not storing browser path when detectBrowserPath is true
  • moved the settings page to a dialog
  • implemented a file picker for the browser path
Screen.Recording.2024-10-17.at.1.28.55.PM.mov

Copy link
Member

@Llandy3d Llandy3d left a comment

Choose a reason for hiding this comment

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

🚀 🚀 🚀

Comment on lines +3 to +19
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(),
})
Copy link
Member

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 🙇 )

@cristianoventura cristianoventura merged commit a622a55 into main Oct 18, 2024
2 checks passed
@cristianoventura cristianoventura deleted the feat/persist-proxy-preferences branch October 18, 2024 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Persist user preferences
3 participants