-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Connect: Generate json schema for app config (#22538)
* Rename `putAllSync` to `writeSync` * Extend `FileStorage` with functions to replace an entire state and return file path used to create it * Add `updateJsonSchema` function * Generate schema for the app config * Always return a string from `createMockFileStorage().getFilePath()` * Add missing license headers * Rename 'teleport_connect_config_schema.json' to 'schema_app_config.json' * Rename `getKeyboardShortcutDescription` to `getShortcutDesc` * Rename `keyboardShortcutSchema` to `shortcutSchema` * Update a valid shortcut message * Rename `configJsonSchemaFile` to `jsonSchema` * Simplify `updateJsonSchema` * Set `$refStrategy` to none * Add missing description for `usageReporting.enabled` * Bump zod to the latest (improves TS performance) * Move `configService` implementation to `configStore` * Rename `configStore` to `configService` * Move `updateJsonSchema` to `createConfigService` * Move `validateStoredConfig` outside `createConfigService` * Rename `createAppConfigSchema.ts` to `appConfigSchema.ts`, `getKeyboardShortcutSchema.ts` to `keyboardShortcutSchema.ts` * Export `createKeyboardShortcutSchema` * Add license header
- Loading branch information
Showing
16 changed files
with
536 additions
and
376 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
206 changes: 206 additions & 0 deletions
206
web/packages/teleterm/src/services/config/appConfigSchema.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
/** | ||
* Copyright 2023 Gravitational, Inc | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { z } from 'zod'; | ||
|
||
import { Platform } from 'teleterm/mainProcess/types'; | ||
|
||
import { createKeyboardShortcutSchema } from './keyboardShortcutSchema'; | ||
|
||
export type AppConfigSchema = ReturnType<typeof createAppConfigSchema>; | ||
export type AppConfig = z.infer<AppConfigSchema>; | ||
|
||
export const createAppConfigSchema = (platform: Platform) => { | ||
const defaultKeymap = getDefaultKeymap(platform); | ||
const defaultTerminalFont = getDefaultTerminalFont(platform); | ||
|
||
const shortcutSchema = createKeyboardShortcutSchema(platform); | ||
|
||
// `keymap.` prefix is used in `initUi.ts` in a predicate function. | ||
return z.object({ | ||
'usageReporting.enabled': z | ||
.boolean() | ||
.default(false) | ||
.describe('Enables collecting of anonymous usage data.'), | ||
'keymap.tab1': shortcutSchema | ||
.default(defaultKeymap['tab1']) | ||
.describe(getShortcutDesc('open tab 1')), | ||
'keymap.tab2': shortcutSchema | ||
.default(defaultKeymap['tab2']) | ||
.describe(getShortcutDesc('open tab 2')), | ||
'keymap.tab3': shortcutSchema | ||
.default(defaultKeymap['tab3']) | ||
.describe(getShortcutDesc('open tab 3')), | ||
'keymap.tab4': shortcutSchema | ||
.default(defaultKeymap['tab4']) | ||
.describe(getShortcutDesc('open tab 4')), | ||
'keymap.tab5': shortcutSchema | ||
.default(defaultKeymap['tab5']) | ||
.describe(getShortcutDesc('open tab 5')), | ||
'keymap.tab6': shortcutSchema | ||
.default(defaultKeymap['tab6']) | ||
.describe(getShortcutDesc('open tab 6')), | ||
'keymap.tab7': shortcutSchema | ||
.default(defaultKeymap['tab7']) | ||
.describe(getShortcutDesc('open tab 7')), | ||
'keymap.tab8': shortcutSchema | ||
.default(defaultKeymap['tab8']) | ||
.describe(getShortcutDesc('open tab 8')), | ||
'keymap.tab9': shortcutSchema | ||
.default(defaultKeymap['tab9']) | ||
.describe(getShortcutDesc('open tab 9')), | ||
'keymap.closeTab': shortcutSchema | ||
.default(defaultKeymap['closeTab']) | ||
.describe(getShortcutDesc('close a tab')), | ||
'keymap.newTab': shortcutSchema | ||
.default(defaultKeymap['newTab']) | ||
.describe(getShortcutDesc('open a new tab')), | ||
'keymap.previousTab': shortcutSchema | ||
.default(defaultKeymap['previousTab']) | ||
.describe(getShortcutDesc('go to the previous tab')), | ||
'keymap.nextTab': shortcutSchema | ||
.default(defaultKeymap['nextTab']) | ||
.describe(getShortcutDesc('go to the next tab')), | ||
'keymap.openConnections': shortcutSchema | ||
.default(defaultKeymap['openConnections']) | ||
.describe(getShortcutDesc('open the connection panel')), | ||
'keymap.openClusters': shortcutSchema | ||
.default(defaultKeymap['openClusters']) | ||
.describe(getShortcutDesc('open the clusters panel')), | ||
'keymap.openProfiles': shortcutSchema | ||
.default(defaultKeymap['openProfiles']) | ||
.describe(getShortcutDesc('open the profiles panel')), | ||
'keymap.openQuickInput': shortcutSchema | ||
.default(defaultKeymap['openQuickInput']) | ||
.describe(getShortcutDesc('open the command bar')), | ||
/** | ||
* This value can be provided by the user and is unsanitized. This means that it cannot be directly interpolated | ||
* in a styled component or used in CSS, as it may inject malicious CSS code. | ||
* Before using it, sanitize it with `CSS.escape` or pass it as a `style` prop. | ||
* Read more https://frontarm.com/james-k-nelson/how-can-i-use-css-in-js-securely/. | ||
*/ | ||
'terminal.fontFamily': z | ||
.string() | ||
.default(defaultTerminalFont) | ||
.describe('Font family for the terminal.'), | ||
'terminal.fontSize': z | ||
.number() | ||
.int() | ||
.min(1) | ||
.max(256) | ||
.default(15) | ||
.describe('Font size for the terminal.'), | ||
}); | ||
}; | ||
|
||
export type KeyboardShortcutAction = | ||
| 'tab1' | ||
| 'tab2' | ||
| 'tab3' | ||
| 'tab4' | ||
| 'tab5' | ||
| 'tab6' | ||
| 'tab7' | ||
| 'tab8' | ||
| 'tab9' | ||
| 'closeTab' | ||
| 'newTab' | ||
| 'previousTab' | ||
| 'nextTab' | ||
| 'openQuickInput' | ||
| 'openConnections' | ||
| 'openClusters' | ||
| 'openProfiles'; | ||
|
||
const getDefaultKeymap = (platform: Platform) => { | ||
switch (platform) { | ||
case 'win32': | ||
return { | ||
tab1: 'Ctrl+1', | ||
tab2: 'Ctrl+2', | ||
tab3: 'Ctrl+3', | ||
tab4: 'Ctrl+4', | ||
tab5: 'Ctrl+5', | ||
tab6: 'Ctrl+6', | ||
tab7: 'Ctrl+7', | ||
tab8: 'Ctrl+8', | ||
tab9: 'Ctrl+9', | ||
closeTab: 'Ctrl+W', | ||
newTab: 'Ctrl+T', | ||
previousTab: 'Ctrl+Shift+Tab', | ||
nextTab: 'Ctrl+Tab', | ||
openQuickInput: 'Ctrl+K', | ||
openConnections: 'Ctrl+P', | ||
openClusters: 'Ctrl+E', | ||
openProfiles: 'Ctrl+I', | ||
}; | ||
case 'linux': | ||
return { | ||
tab1: 'Alt+1', | ||
tab2: 'Alt+2', | ||
tab3: 'Alt+3', | ||
tab4: 'Alt+4', | ||
tab5: 'Alt+5', | ||
tab6: 'Alt+6', | ||
tab7: 'Alt+7', | ||
tab8: 'Alt+8', | ||
tab9: 'Alt+9', | ||
closeTab: 'Ctrl+W', | ||
newTab: 'Ctrl+T', | ||
previousTab: 'Ctrl+Shift+Tab', | ||
nextTab: 'Ctrl+Tab', | ||
openQuickInput: 'Ctrl+K', | ||
openConnections: 'Ctrl+P', | ||
openClusters: 'Ctrl+E', | ||
openProfiles: 'Ctrl+I', | ||
}; | ||
case 'darwin': | ||
return { | ||
tab1: 'Command+1', | ||
tab2: 'Command+2', | ||
tab3: 'Command+3', | ||
tab4: 'Command+4', | ||
tab5: 'Command+5', | ||
tab6: 'Command+6', | ||
tab7: 'Command+7', | ||
tab8: 'Command+8', | ||
tab9: 'Command+9', | ||
closeTab: 'Command+W', | ||
newTab: 'Command+T', | ||
previousTab: 'Control+Shift+Tab', | ||
nextTab: 'Control+Tab', | ||
openQuickInput: 'Command+K', | ||
openConnections: 'Command+P', | ||
openClusters: 'Command+E', | ||
openProfiles: 'Command+I', | ||
}; | ||
} | ||
}; | ||
|
||
function getDefaultTerminalFont(platform: Platform) { | ||
switch (platform) { | ||
case 'win32': | ||
return "'Consolas', 'Courier New', monospace"; | ||
case 'linux': | ||
return "'Droid Sans Mono', 'Courier New', monospace, 'Droid Sans Fallback'"; | ||
case 'darwin': | ||
return "Menlo, Monaco, 'Courier New', monospace"; | ||
} | ||
} | ||
|
||
function getShortcutDesc(actionDesc: string): string { | ||
return `Shortcut to ${actionDesc}. A valid shortcut contains at least one modifier and a single key code, for example "Shift+Tab". Function keys do not require a modifier.`; | ||
} |
105 changes: 105 additions & 0 deletions
105
web/packages/teleterm/src/services/config/configService.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/** | ||
* Copyright 2023 Gravitational, Inc | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import Logger, { NullService } from 'teleterm/logger'; | ||
import { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mocks'; | ||
|
||
import { createConfigService } from './configService'; | ||
|
||
beforeAll(() => { | ||
Logger.init(new NullService()); | ||
}); | ||
|
||
test('stored and default values are combined', () => { | ||
const configFile = createMockFileStorage(); | ||
configFile.put('usageReporting.enabled', true); | ||
const configService = createConfigService({ | ||
configFile, | ||
jsonSchemaFile: createMockFileStorage(), | ||
platform: 'darwin', | ||
}); | ||
|
||
expect(configService.getStoredConfigErrors()).toBeUndefined(); | ||
|
||
const usageReportingEnabled = configService.get('usageReporting.enabled'); | ||
expect(usageReportingEnabled.value).toBe(true); | ||
expect(usageReportingEnabled.metadata.isStored).toBe(true); | ||
|
||
const terminalFontSize = configService.get('terminal.fontSize'); | ||
expect(terminalFontSize.value).toBe(15); | ||
expect(terminalFontSize.metadata.isStored).toBe(false); | ||
}); | ||
|
||
test('in case of invalid value a default one is returned', () => { | ||
const configFile = createMockFileStorage(); | ||
configFile.put('usageReporting.enabled', 'abcde'); | ||
const configService = createConfigService({ | ||
configFile: configFile, | ||
jsonSchemaFile: createMockFileStorage(), | ||
platform: 'darwin', | ||
}); | ||
|
||
expect(configService.getStoredConfigErrors()).toStrictEqual([ | ||
{ | ||
code: 'invalid_type', | ||
expected: 'boolean', | ||
received: 'string', | ||
message: 'Expected boolean, received string', | ||
path: ['usageReporting.enabled'], | ||
}, | ||
]); | ||
|
||
const usageReportingEnabled = configService.get('usageReporting.enabled'); | ||
expect(usageReportingEnabled.value).toBe(false); | ||
expect(usageReportingEnabled.metadata.isStored).toBe(false); | ||
|
||
const terminalFontSize = configService.get('terminal.fontSize'); | ||
expect(terminalFontSize.value).toBe(15); | ||
expect(terminalFontSize.metadata.isStored).toBe(false); | ||
}); | ||
|
||
test('calling set updated the value in store', () => { | ||
const configFile = createMockFileStorage(); | ||
const configService = createConfigService({ | ||
configFile, | ||
jsonSchemaFile: createMockFileStorage(), | ||
platform: 'darwin', | ||
}); | ||
|
||
configService.set('usageReporting.enabled', true); | ||
|
||
const usageReportingEnabled = configService.get('usageReporting.enabled'); | ||
expect(usageReportingEnabled.value).toBe(true); | ||
expect(usageReportingEnabled.metadata.isStored).toBe(true); | ||
}); | ||
|
||
test('field linking to the json schema and the json schema itself are updated', () => { | ||
const configFile = createMockFileStorage(); | ||
const jsonSchemaFile = createMockFileStorage({ | ||
filePath: '~/config_schema.json', | ||
}); | ||
|
||
jest.spyOn(jsonSchemaFile, 'replace'); | ||
|
||
createConfigService({ | ||
configFile, | ||
jsonSchemaFile, | ||
platform: 'darwin', | ||
}); | ||
|
||
expect(configFile.get('$schema')).toBe('config_schema.json'); | ||
expect(jsonSchemaFile.replace).toHaveBeenCalledTimes(1); | ||
}); |
Oops, something went wrong.