From b0a68cb4e6e30b4dab0e0800e96b980f70a4e052 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Thu, 2 Mar 2023 12:41:44 +0100 Subject: [PATCH 01/22] Rename `putAllSync` to `writeSync` --- web/packages/teleterm/src/main.ts | 2 +- web/packages/teleterm/src/mainProcess/types.ts | 2 +- .../teleterm/src/services/fileStorage/fileStorage.ts | 7 ++++--- .../src/services/fileStorage/fileStorageClient.ts | 8 ++++---- .../teleterm/src/services/fileStorage/fixtures/mocks.ts | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index 7805585bc4174..ee72c3fc8c465 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -92,7 +92,7 @@ function initializeApp(): void { app.on('will-quit', async event => { event.preventDefault(); - appStateFileStorage.putAllSync(); + appStateFileStorage.writeSync(); globalShortcut.unregisterAll(); try { await mainProcess.dispose(); diff --git a/web/packages/teleterm/src/mainProcess/types.ts b/web/packages/teleterm/src/mainProcess/types.ts index 214706bb049f6..7ac5ac664e020 100644 --- a/web/packages/teleterm/src/mainProcess/types.ts +++ b/web/packages/teleterm/src/mainProcess/types.ts @@ -131,5 +131,5 @@ export enum ConfigServiceEventType { export enum FileStorageEventType { Get = 'Get', Put = 'Put', - PutAllSync = 'PutAllSync', + WriteSync = 'WriteSync', } diff --git a/web/packages/teleterm/src/services/fileStorage/fileStorage.ts b/web/packages/teleterm/src/services/fileStorage/fileStorage.ts index 83e72dd57a713..bb9b32e88b38d 100644 --- a/web/packages/teleterm/src/services/fileStorage/fileStorage.ts +++ b/web/packages/teleterm/src/services/fileStorage/fileStorage.ts @@ -25,7 +25,8 @@ const logger = new Logger('FileStorage'); export interface FileStorage { put(path: string, json: any): void; - putAllSync(): void; + /** Synchronously writes the storage state to disk. */ + writeSync(): void; get(path?: string): T; } @@ -50,7 +51,7 @@ export function createFileStorage(opts: { : writeFile(filePath, text); } - function putAllSync() { + function writeSync(): void { const text = stringify(state); try { fs.writeFileSync(filePath, text); @@ -65,7 +66,7 @@ export function createFileStorage(opts: { return { put, - putAllSync, + writeSync, get, }; } diff --git a/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts b/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts index c9ffff816b688..0f23eeab43301 100644 --- a/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts +++ b/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts @@ -32,8 +32,8 @@ export function subscribeToFileStorageEvents(configService: FileStorage): void { return (event.returnValue = configService.get(item.path)); case FileStorageEventType.Put: return configService.put(item.path, item.json); - case FileStorageEventType.PutAllSync: - return configService.putAllSync(); + case FileStorageEventType.WriteSync: + return configService.writeSync(); } } ); @@ -50,10 +50,10 @@ export function createFileStorageClient(): FileStorage { path, json, }), - putAllSync: () => + writeSync: () => ipcRenderer.send( FileStorageEventChannel, - FileStorageEventType.PutAllSync, + FileStorageEventType.WriteSync, {} ), }; diff --git a/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts b/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts index 5fce1077ffe17..e004e74766190 100644 --- a/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts @@ -27,6 +27,6 @@ export function createMockFileStorage(): FileStorage { return key ? state[key] : (state as T); }, - putAllSync() {}, + writeSync() {}, }; } From e43f4481a21c0d7554120036bca1ccac5b47fbe6 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Thu, 2 Mar 2023 14:06:51 +0100 Subject: [PATCH 02/22] Extend `FileStorage` with functions to replace an entire state and return file path used to create it --- .../teleterm/src/mainProcess/types.ts | 2 + .../src/services/fileStorage/fileStorage.ts | 41 +++++++++++++++---- .../services/fileStorage/fileStorageClient.ts | 26 +++++++++--- .../services/fileStorage/fixtures/mocks.ts | 16 ++++++-- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/web/packages/teleterm/src/mainProcess/types.ts b/web/packages/teleterm/src/mainProcess/types.ts index 7ac5ac664e020..12e06edd89fee 100644 --- a/web/packages/teleterm/src/mainProcess/types.ts +++ b/web/packages/teleterm/src/mainProcess/types.ts @@ -132,4 +132,6 @@ export enum FileStorageEventType { Get = 'Get', Put = 'Put', WriteSync = 'WriteSync', + Replace = 'Replace', + GetFilePath = 'GetFilePath', } diff --git a/web/packages/teleterm/src/services/fileStorage/fileStorage.ts b/web/packages/teleterm/src/services/fileStorage/fileStorage.ts index bb9b32e88b38d..94455aa9af25e 100644 --- a/web/packages/teleterm/src/services/fileStorage/fileStorage.ts +++ b/web/packages/teleterm/src/services/fileStorage/fileStorage.ts @@ -23,12 +23,20 @@ import Logger from 'teleterm/logger'; const logger = new Logger('FileStorage'); export interface FileStorage { - put(path: string, json: any): void; + /** Asynchronously updates value for a given key. */ + put(key: string, json: any): void; + + /** Asynchronously replaces the entire storage state with a new value. */ + replace(json: any): void; /** Synchronously writes the storage state to disk. */ writeSync(): void; - get(path?: string): T; + /** Returns value for a given key. If the key is omitted, the entire storage state is returned. */ + get(key?: string): T; + + /** Returns the file path used to create the storage. */ + getFilePath(): string; } export function createFileStorage(opts: { @@ -40,15 +48,11 @@ export function createFileStorage(opts: { } const { filePath } = opts; - const state = loadState(opts.filePath); + let state = loadState(opts.filePath); - function put(key: string, json: any) { + function put(key: string, json: any): void { state[key] = json; - const text = stringify(state); - - opts.debounceWrites - ? writeFileDebounced(filePath, text) - : writeFile(filePath, text); + stringifyAndWrite(); } function writeSync(): void { @@ -64,10 +68,29 @@ export function createFileStorage(opts: { return key ? state[key] : (state as T); } + function replace(json: any): void { + state = json; + stringifyAndWrite(); + } + + function getFilePath(): string { + return opts.filePath; + } + + function stringifyAndWrite(): void { + const text = stringify(state); + + opts.debounceWrites + ? writeFileDebounced(filePath, text) + : writeFile(filePath, text); + } + return { put, writeSync, get, + replace, + getFilePath, }; } diff --git a/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts b/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts index 0f23eeab43301..8be82f4b8e296 100644 --- a/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts +++ b/web/packages/teleterm/src/services/fileStorage/fileStorageClient.ts @@ -29,11 +29,15 @@ export function subscribeToFileStorageEvents(configService: FileStorage): void { (event, eventType: FileStorageEventType, item) => { switch (eventType) { case FileStorageEventType.Get: - return (event.returnValue = configService.get(item.path)); + return (event.returnValue = configService.get(item.key)); case FileStorageEventType.Put: - return configService.put(item.path, item.json); + return configService.put(item.key, item.json); case FileStorageEventType.WriteSync: return configService.writeSync(); + case FileStorageEventType.Replace: + return configService.replace(item.json); + case FileStorageEventType.GetFilePath: + return configService.getFilePath(); } } ); @@ -41,13 +45,13 @@ export function subscribeToFileStorageEvents(configService: FileStorage): void { export function createFileStorageClient(): FileStorage { return { - get: path => + get: key => ipcRenderer.sendSync(FileStorageEventChannel, FileStorageEventType.Get, { - path, + key, }), - put: (path, json) => + put: (key, json) => ipcRenderer.send(FileStorageEventChannel, FileStorageEventType.Put, { - path, + key, json, }), writeSync: () => @@ -56,5 +60,15 @@ export function createFileStorageClient(): FileStorage { FileStorageEventType.WriteSync, {} ), + replace: json => + ipcRenderer.send(FileStorageEventChannel, FileStorageEventType.Replace, { + json, + }), + getFilePath: () => + ipcRenderer.sendSync( + FileStorageEventChannel, + FileStorageEventType.GetFilePath, + {} + ), }; } diff --git a/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts b/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts index e004e74766190..966e8965e5d91 100644 --- a/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts @@ -16,11 +16,13 @@ import { FileStorage } from 'teleterm/services/fileStorage'; -export function createMockFileStorage(): FileStorage { +export function createMockFileStorage(opts?: { + filePath: string; +}): FileStorage { let state = {}; return { - put(path: string, json: any) { - state[path] = json; + put(key: string, json: any) { + state[key] = json; }, get(key?: string): T { @@ -28,5 +30,13 @@ export function createMockFileStorage(): FileStorage { }, writeSync() {}, + + replace(json: any) { + state = json; + }, + + getFilePath(): string { + return opts?.filePath; + }, }; } From eda4004d27ebdf8de9768e116f9b7ff676c0322e Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Thu, 2 Mar 2023 14:07:36 +0100 Subject: [PATCH 03/22] Add `updateJsonSchema` function --- web/packages/teleterm/package.json | 3 +- .../services/config/updateJsonSchema.test.ts | 46 +++++++++++++++++++ .../src/services/config/updateJsonSchema.ts | 39 ++++++++++++++++ yarn.lock | 13 ++++-- 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 web/packages/teleterm/src/services/config/updateJsonSchema.test.ts create mode 100644 web/packages/teleterm/src/services/config/updateJsonSchema.ts diff --git a/web/packages/teleterm/package.json b/web/packages/teleterm/package.json index e1b0f58bc4509..aabeee9a36fe7 100644 --- a/web/packages/teleterm/package.json +++ b/web/packages/teleterm/package.json @@ -56,7 +56,8 @@ "winston": "^3.3.3", "xterm": "^5.0.0", "xterm-addon-fit": "^0.7.0", - "zod": "^3.20.0" + "zod": "^3.20.6", + "zod-to-json-schema": "^3.20.4" }, "productName": "Teleport Connect" } diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts new file mode 100644 index 0000000000000..3ce323379a8af --- /dev/null +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +import { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mocks'; + +import { updateJsonSchema } from './updateJsonSchema'; + +const schema = z.object({ + 'fonts.monoFamily': z.string().default('Arial'), + 'usageReporting.enabled': z.boolean().default(false), +}); + +const generatedJsonSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + $schema: { + type: 'string', + }, + 'fonts.monoFamily': { + default: 'Arial', + type: 'string', + }, + 'usageReporting.enabled': { + default: false, + type: 'boolean', + }, + }, + required: ['$schema'], + type: 'object', +}; + +test('field linking to the schema and the schema itself are updated', () => { + const configFile = createMockFileStorage(); + const configJsonSchemaFile = createMockFileStorage({ + filePath: '~/config_schema.json', + }); + + updateJsonSchema({ + configSchema: schema, + configFile: configFile, + configJsonSchemaFile: configJsonSchemaFile, + }); + + expect(configFile.get('$schema')).toBe('config_schema.json'); + expect(configJsonSchemaFile.get()).toEqual(generatedJsonSchema); +}); diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.ts new file mode 100644 index 0000000000000..bb7ef70b83c91 --- /dev/null +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.ts @@ -0,0 +1,39 @@ +import path from 'path'; + +import { z } from 'zod'; + +import zodToJsonSchema from 'zod-to-json-schema'; + +import { FileStorage } from 'teleterm/services/fileStorage'; + +export function updateJsonSchema({ + configSchema, + configFile, + configJsonSchemaFile, +}: { + configSchema: z.AnyZodObject; + configFile: FileStorage; + configJsonSchemaFile: FileStorage; +}): void { + //adds $schema field to the original schema to prevent marking it as a not allowed property + const configSchemaWithSchemaField = configSchema.extend({ + $schema: z.string(), + }); + const jsonSchema = zodToJsonSchema(configSchemaWithSchemaField); + + configJsonSchemaFile.replace(jsonSchema); + linkToJsonSchemaIfNeeded( + configFile, + path.basename(configJsonSchemaFile.getFilePath()) + ); +} + +function linkToJsonSchemaIfNeeded( + configFileStorage: FileStorage, + jsonSchemaFilename: string +): void { + const schemaField = configFileStorage.get('$schema'); + if (schemaField !== jsonSchemaFilename) { + configFileStorage.put('$schema', jsonSchemaFilename); + } +} diff --git a/yarn.lock b/yarn.lock index 3589c3b9d5620..3b1236e8df6fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14996,10 +14996,15 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@^3.20.0: - version "3.20.2" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.2.tgz#068606642c8f51b3333981f91c0a8ab37dfc2807" - integrity sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ== +zod-to-json-schema@^3.20.4: + version "3.20.4" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.20.4.tgz#155f687c5a059fdc0f1bb3ff32d6e9200036b6f4" + integrity sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg== + +zod@^3.20.6: + version "3.20.6" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.6.tgz#2f2f08ff81291d47d99e86140fedb4e0db08361a" + integrity sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA== zone.js@^0.11.0: version "0.11.8" From 53867ac343b5e4e1b169cf82c477bd674d641506 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Thu, 2 Mar 2023 14:08:20 +0100 Subject: [PATCH 04/22] Generate schema for the app config --- web/packages/teleterm/src/main.ts | 19 ++- .../src/mainProcess/fixtures/mocks.ts | 9 +- .../src/services/config/configService.ts | 124 ++++++++++++------ 3 files changed, 104 insertions(+), 48 deletions(-) diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index ee72c3fc8c465..450e770fbe0f1 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -47,14 +47,23 @@ function initializeApp(): void { filePath: path.join(settings.userDataDir, 'app_state.json'), debounceWrites: true, }); - const appConfigFileStorage = createFileStorage({ + const configFileStorage = createFileStorage({ filePath: path.join(settings.userDataDir, 'app_config.json'), debounceWrites: false, }); - const configService = createConfigService( - appConfigFileStorage, - settings.platform - ); + const configJsonSchemaFileStorage = createFileStorage({ + filePath: path.join( + settings.userDataDir, + 'teleport_connect_config_schema.json' + ), + debounceWrites: false, + }); + + const configService = createConfigService({ + configFile: configFileStorage, + configJsonSchemaFile: configJsonSchemaFileStorage, + platform: settings.platform, + }); const windowsManager = new WindowsManager(appStateFileStorage, settings); process.on('uncaughtException', error => { diff --git a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts index ead746ab08572..4d6d531a2ed3a 100644 --- a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts +++ b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts @@ -25,10 +25,11 @@ export class MockMainProcessClient implements MainProcessClient { configService: ReturnType; constructor(private runtimeSettings: Partial = {}) { - this.configService = createConfigService( - createMockFileStorage(), - this.getRuntimeSettings().platform - ); + this.configService = createConfigService({ + configFile: createMockFileStorage(), + configJsonSchemaFile: createMockFileStorage(), + platform: this.getRuntimeSettings().platform, + }); } getRuntimeSettings(): RuntimeSettings { diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index 3d229f5973c73..1d61ebffba230 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -21,6 +21,7 @@ import { Platform } from 'teleterm/mainProcess/types'; import { createConfigStore } from './configStore'; import { getKeyboardShortcutSchema } from './getKeyboardShortcutSchema'; +import { updateJsonSchema } from './updateJsonSchema'; const createAppConfigSchema = (platform: Platform) => { const defaultKeymap = getDefaultKeymap(platform); @@ -31,43 +32,74 @@ const createAppConfigSchema = (platform: Platform) => { // `keymap.` prefix is used in `initUi.ts` in a predicate function. return z.object({ 'usageReporting.enabled': z.boolean().default(false), - 'keymap.tab1': keyboardShortcutSchema.default(defaultKeymap['tab1']), - 'keymap.tab2': keyboardShortcutSchema.default(defaultKeymap['tab2']), - 'keymap.tab3': keyboardShortcutSchema.default(defaultKeymap['tab3']), - 'keymap.tab4': keyboardShortcutSchema.default(defaultKeymap['tab4']), - 'keymap.tab5': keyboardShortcutSchema.default(defaultKeymap['tab5']), - 'keymap.tab6': keyboardShortcutSchema.default(defaultKeymap['tab6']), - 'keymap.tab7': keyboardShortcutSchema.default(defaultKeymap['tab7']), - 'keymap.tab8': keyboardShortcutSchema.default(defaultKeymap['tab8']), - 'keymap.tab9': keyboardShortcutSchema.default(defaultKeymap['tab9']), - 'keymap.closeTab': keyboardShortcutSchema.default( - defaultKeymap['closeTab'] - ), - 'keymap.newTab': keyboardShortcutSchema.default(defaultKeymap['newTab']), - 'keymap.previousTab': keyboardShortcutSchema.default( - defaultKeymap['previousTab'] - ), - 'keymap.nextTab': keyboardShortcutSchema.default(defaultKeymap['nextTab']), - 'keymap.openConnections': keyboardShortcutSchema.default( - defaultKeymap['openConnections'] - ), - 'keymap.openClusters': keyboardShortcutSchema.default( - defaultKeymap['openClusters'] - ), - 'keymap.openProfiles': keyboardShortcutSchema.default( - defaultKeymap['openProfiles'] - ), - 'keymap.openQuickInput': keyboardShortcutSchema.default( - defaultKeymap['openQuickInput'] - ), + 'keymap.tab1': keyboardShortcutSchema + .default(defaultKeymap['tab1']) + .describe(getKeyboardShortcutDescription('open tab 1')), + 'keymap.tab2': keyboardShortcutSchema + .default(defaultKeymap['tab2']) + .describe(getKeyboardShortcutDescription('open tab 2')), + 'keymap.tab3': keyboardShortcutSchema + .default(defaultKeymap['tab3']) + .describe(getKeyboardShortcutDescription('open tab 3')), + 'keymap.tab4': keyboardShortcutSchema + .default(defaultKeymap['tab4']) + .describe(getKeyboardShortcutDescription('open tab 4')), + 'keymap.tab5': keyboardShortcutSchema + .default(defaultKeymap['tab5']) + .describe(getKeyboardShortcutDescription('open tab 5')), + 'keymap.tab6': keyboardShortcutSchema + .default(defaultKeymap['tab6']) + .describe(getKeyboardShortcutDescription('open tab 6')), + 'keymap.tab7': keyboardShortcutSchema + .default(defaultKeymap['tab7']) + .describe(getKeyboardShortcutDescription('open tab 7')), + 'keymap.tab8': keyboardShortcutSchema + .default(defaultKeymap['tab8']) + .describe(getKeyboardShortcutDescription('open tab 8')), + 'keymap.tab9': keyboardShortcutSchema + .default(defaultKeymap['tab9']) + .describe(getKeyboardShortcutDescription('open tab 9')), + 'keymap.closeTab': keyboardShortcutSchema + .default(defaultKeymap['closeTab']) + .describe(getKeyboardShortcutDescription('close a tab')), + 'keymap.newTab': keyboardShortcutSchema + .default(defaultKeymap['newTab']) + .describe(getKeyboardShortcutDescription('open a new tab')), + 'keymap.previousTab': keyboardShortcutSchema + .default(defaultKeymap['previousTab']) + .describe(getKeyboardShortcutDescription('go to the previous tab')), + 'keymap.nextTab': keyboardShortcutSchema + .default(defaultKeymap['nextTab']) + .describe(getKeyboardShortcutDescription('go to the next tab')), + 'keymap.openConnections': keyboardShortcutSchema + .default(defaultKeymap['openConnections']) + .describe(getKeyboardShortcutDescription('open the connection panel')), + 'keymap.openClusters': keyboardShortcutSchema + .default(defaultKeymap['openClusters']) + .describe(getKeyboardShortcutDescription('open the clusters panel')), + 'keymap.openProfiles': keyboardShortcutSchema + .default(defaultKeymap['openProfiles']) + .describe(getKeyboardShortcutDescription('open the profiles panel')), + 'keymap.openQuickInput': keyboardShortcutSchema + .default(defaultKeymap['openQuickInput']) + .describe(getKeyboardShortcutDescription('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), - 'terminal.fontSize': z.number().int().min(1).max(256).default(15), + '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.'), }); }; @@ -168,14 +200,28 @@ function getDefaultTerminalFont(platform: Platform) { } } -export function createConfigService( - appConfigFileStorage: FileStorage, - platform: Platform -) { - return createConfigStore( - createAppConfigSchema(platform), - appConfigFileStorage - ); +function getKeyboardShortcutDescription(about: string): string { + return `Shortcut to ${about}. A valid shortcut contains at least one modifier and a single key code, for example "Ctrl+Shift+A". Function keys do not require a modifier.`; +} + +export function createConfigService({ + configFile, + configJsonSchemaFile, + platform, +}: { + configFile: FileStorage; + configJsonSchemaFile: FileStorage; + platform: Platform; +}) { + const schema = createAppConfigSchema(platform); + + updateJsonSchema({ + configSchema: schema, + configFile, + configJsonSchemaFile, + }); + + return createConfigStore(schema, configFile); } export type ConfigService = ReturnType; From 43e4a2e5c448569bc29ec65acd39d768a953f451 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Fri, 3 Mar 2023 15:09:18 +0100 Subject: [PATCH 05/22] Always return a string from `createMockFileStorage().getFilePath()` --- .../teleterm/src/services/fileStorage/fixtures/mocks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts b/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts index 966e8965e5d91..09cd7c90bd10a 100644 --- a/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/fileStorage/fixtures/mocks.ts @@ -36,7 +36,7 @@ export function createMockFileStorage(opts?: { }, getFilePath(): string { - return opts?.filePath; + return opts?.filePath || ''; }, }; } From e4ca2b824b9ae6396de0ef99e4aa8b76a90cf9aa Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Fri, 3 Mar 2023 15:29:47 +0100 Subject: [PATCH 06/22] Add missing license headers --- .../src/services/config/updateJsonSchema.test.ts | 16 ++++++++++++++++ .../src/services/config/updateJsonSchema.ts | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts index 3ce323379a8af..8520b13f08e91 100644 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts @@ -1,3 +1,19 @@ +/** + * 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 { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mocks'; diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.ts index bb7ef70b83c91..2b8c3ae9236f0 100644 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.ts +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.ts @@ -1,3 +1,19 @@ +/** + * 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 path from 'path'; import { z } from 'zod'; From 949bb0611948070f74df6c3bc4cbafb000418cd2 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 10:14:10 +0100 Subject: [PATCH 07/22] Rename 'teleport_connect_config_schema.json' to 'schema_app_config.json' --- web/packages/teleterm/src/main.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index 450e770fbe0f1..4c95f73b12bf1 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -52,10 +52,7 @@ function initializeApp(): void { debounceWrites: false, }); const configJsonSchemaFileStorage = createFileStorage({ - filePath: path.join( - settings.userDataDir, - 'teleport_connect_config_schema.json' - ), + filePath: path.join(settings.userDataDir, 'schema_app_config.json'), debounceWrites: false, }); From c64b61e81d339cbf4f9334f0e82d29132b1c9db5 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 10:19:12 +0100 Subject: [PATCH 08/22] Rename `getKeyboardShortcutDescription` to `getShortcutDesc` --- .../src/services/config/configService.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index 1d61ebffba230..9d43b483b43f2 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -34,55 +34,55 @@ const createAppConfigSchema = (platform: Platform) => { 'usageReporting.enabled': z.boolean().default(false), 'keymap.tab1': keyboardShortcutSchema .default(defaultKeymap['tab1']) - .describe(getKeyboardShortcutDescription('open tab 1')), + .describe(getShortcutDesc('open tab 1')), 'keymap.tab2': keyboardShortcutSchema .default(defaultKeymap['tab2']) - .describe(getKeyboardShortcutDescription('open tab 2')), + .describe(getShortcutDesc('open tab 2')), 'keymap.tab3': keyboardShortcutSchema .default(defaultKeymap['tab3']) - .describe(getKeyboardShortcutDescription('open tab 3')), + .describe(getShortcutDesc('open tab 3')), 'keymap.tab4': keyboardShortcutSchema .default(defaultKeymap['tab4']) - .describe(getKeyboardShortcutDescription('open tab 4')), + .describe(getShortcutDesc('open tab 4')), 'keymap.tab5': keyboardShortcutSchema .default(defaultKeymap['tab5']) - .describe(getKeyboardShortcutDescription('open tab 5')), + .describe(getShortcutDesc('open tab 5')), 'keymap.tab6': keyboardShortcutSchema .default(defaultKeymap['tab6']) - .describe(getKeyboardShortcutDescription('open tab 6')), + .describe(getShortcutDesc('open tab 6')), 'keymap.tab7': keyboardShortcutSchema .default(defaultKeymap['tab7']) - .describe(getKeyboardShortcutDescription('open tab 7')), + .describe(getShortcutDesc('open tab 7')), 'keymap.tab8': keyboardShortcutSchema .default(defaultKeymap['tab8']) - .describe(getKeyboardShortcutDescription('open tab 8')), + .describe(getShortcutDesc('open tab 8')), 'keymap.tab9': keyboardShortcutSchema .default(defaultKeymap['tab9']) - .describe(getKeyboardShortcutDescription('open tab 9')), + .describe(getShortcutDesc('open tab 9')), 'keymap.closeTab': keyboardShortcutSchema .default(defaultKeymap['closeTab']) - .describe(getKeyboardShortcutDescription('close a tab')), + .describe(getShortcutDesc('close a tab')), 'keymap.newTab': keyboardShortcutSchema .default(defaultKeymap['newTab']) - .describe(getKeyboardShortcutDescription('open a new tab')), + .describe(getShortcutDesc('open a new tab')), 'keymap.previousTab': keyboardShortcutSchema .default(defaultKeymap['previousTab']) - .describe(getKeyboardShortcutDescription('go to the previous tab')), + .describe(getShortcutDesc('go to the previous tab')), 'keymap.nextTab': keyboardShortcutSchema .default(defaultKeymap['nextTab']) - .describe(getKeyboardShortcutDescription('go to the next tab')), + .describe(getShortcutDesc('go to the next tab')), 'keymap.openConnections': keyboardShortcutSchema .default(defaultKeymap['openConnections']) - .describe(getKeyboardShortcutDescription('open the connection panel')), + .describe(getShortcutDesc('open the connection panel')), 'keymap.openClusters': keyboardShortcutSchema .default(defaultKeymap['openClusters']) - .describe(getKeyboardShortcutDescription('open the clusters panel')), + .describe(getShortcutDesc('open the clusters panel')), 'keymap.openProfiles': keyboardShortcutSchema .default(defaultKeymap['openProfiles']) - .describe(getKeyboardShortcutDescription('open the profiles panel')), + .describe(getShortcutDesc('open the profiles panel')), 'keymap.openQuickInput': keyboardShortcutSchema .default(defaultKeymap['openQuickInput']) - .describe(getKeyboardShortcutDescription('open the command bar')), + .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. @@ -200,8 +200,8 @@ function getDefaultTerminalFont(platform: Platform) { } } -function getKeyboardShortcutDescription(about: string): string { - return `Shortcut to ${about}. A valid shortcut contains at least one modifier and a single key code, for example "Ctrl+Shift+A". Function keys do not require a modifier.`; +function getShortcutDesc(actionDesc: string): string { + return `Shortcut to ${actionDesc}. A valid shortcut contains at least one modifier and a single key code, for example "Ctrl+Shift+A". Function keys do not require a modifier.`; } export function createConfigService({ From 1c38840276dbbd0451e6decc2ed36fa8d98d0a49 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 10:20:04 +0100 Subject: [PATCH 09/22] Rename `keyboardShortcutSchema` to `shortcutSchema` --- .../src/services/config/configService.ts | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index 9d43b483b43f2..ed6fd114dfe69 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -27,60 +27,60 @@ const createAppConfigSchema = (platform: Platform) => { const defaultKeymap = getDefaultKeymap(platform); const defaultTerminalFont = getDefaultTerminalFont(platform); - const keyboardShortcutSchema = getKeyboardShortcutSchema(platform); + const shortcutSchema = getKeyboardShortcutSchema(platform); // `keymap.` prefix is used in `initUi.ts` in a predicate function. return z.object({ 'usageReporting.enabled': z.boolean().default(false), - 'keymap.tab1': keyboardShortcutSchema + 'keymap.tab1': shortcutSchema .default(defaultKeymap['tab1']) .describe(getShortcutDesc('open tab 1')), - 'keymap.tab2': keyboardShortcutSchema + 'keymap.tab2': shortcutSchema .default(defaultKeymap['tab2']) .describe(getShortcutDesc('open tab 2')), - 'keymap.tab3': keyboardShortcutSchema + 'keymap.tab3': shortcutSchema .default(defaultKeymap['tab3']) .describe(getShortcutDesc('open tab 3')), - 'keymap.tab4': keyboardShortcutSchema + 'keymap.tab4': shortcutSchema .default(defaultKeymap['tab4']) .describe(getShortcutDesc('open tab 4')), - 'keymap.tab5': keyboardShortcutSchema + 'keymap.tab5': shortcutSchema .default(defaultKeymap['tab5']) .describe(getShortcutDesc('open tab 5')), - 'keymap.tab6': keyboardShortcutSchema + 'keymap.tab6': shortcutSchema .default(defaultKeymap['tab6']) .describe(getShortcutDesc('open tab 6')), - 'keymap.tab7': keyboardShortcutSchema + 'keymap.tab7': shortcutSchema .default(defaultKeymap['tab7']) .describe(getShortcutDesc('open tab 7')), - 'keymap.tab8': keyboardShortcutSchema + 'keymap.tab8': shortcutSchema .default(defaultKeymap['tab8']) .describe(getShortcutDesc('open tab 8')), - 'keymap.tab9': keyboardShortcutSchema + 'keymap.tab9': shortcutSchema .default(defaultKeymap['tab9']) .describe(getShortcutDesc('open tab 9')), - 'keymap.closeTab': keyboardShortcutSchema + 'keymap.closeTab': shortcutSchema .default(defaultKeymap['closeTab']) .describe(getShortcutDesc('close a tab')), - 'keymap.newTab': keyboardShortcutSchema + 'keymap.newTab': shortcutSchema .default(defaultKeymap['newTab']) .describe(getShortcutDesc('open a new tab')), - 'keymap.previousTab': keyboardShortcutSchema + 'keymap.previousTab': shortcutSchema .default(defaultKeymap['previousTab']) .describe(getShortcutDesc('go to the previous tab')), - 'keymap.nextTab': keyboardShortcutSchema + 'keymap.nextTab': shortcutSchema .default(defaultKeymap['nextTab']) .describe(getShortcutDesc('go to the next tab')), - 'keymap.openConnections': keyboardShortcutSchema + 'keymap.openConnections': shortcutSchema .default(defaultKeymap['openConnections']) .describe(getShortcutDesc('open the connection panel')), - 'keymap.openClusters': keyboardShortcutSchema + 'keymap.openClusters': shortcutSchema .default(defaultKeymap['openClusters']) .describe(getShortcutDesc('open the clusters panel')), - 'keymap.openProfiles': keyboardShortcutSchema + 'keymap.openProfiles': shortcutSchema .default(defaultKeymap['openProfiles']) .describe(getShortcutDesc('open the profiles panel')), - 'keymap.openQuickInput': keyboardShortcutSchema + 'keymap.openQuickInput': shortcutSchema .default(defaultKeymap['openQuickInput']) .describe(getShortcutDesc('open the command bar')), /** From 2ddd5c317567f578d7909a34999f3c0234f4fb97 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 10:21:36 +0100 Subject: [PATCH 10/22] Update a valid shortcut message --- web/packages/teleterm/src/services/config/configService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index ed6fd114dfe69..256694489c289 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -201,7 +201,7 @@ function getDefaultTerminalFont(platform: Platform) { } function getShortcutDesc(actionDesc: string): string { - return `Shortcut to ${actionDesc}. A valid shortcut contains at least one modifier and a single key code, for example "Ctrl+Shift+A". Function keys do not require a modifier.`; + 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.`; } export function createConfigService({ From 4168cbefa0dfb58e7423e40617032324b5ddc6d4 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 10:25:24 +0100 Subject: [PATCH 11/22] Rename `configJsonSchemaFile` to `jsonSchema` --- web/packages/teleterm/src/main.ts | 2 +- web/packages/teleterm/src/mainProcess/fixtures/mocks.ts | 2 +- .../teleterm/src/services/config/configService.ts | 6 +++--- .../teleterm/src/services/config/updateJsonSchema.test.ts | 2 +- .../teleterm/src/services/config/updateJsonSchema.ts | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index 4c95f73b12bf1..e780a92ae86e0 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -58,7 +58,7 @@ function initializeApp(): void { const configService = createConfigService({ configFile: configFileStorage, - configJsonSchemaFile: configJsonSchemaFileStorage, + jsonSchemaFile: configJsonSchemaFileStorage, platform: settings.platform, }); const windowsManager = new WindowsManager(appStateFileStorage, settings); diff --git a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts index 4d6d531a2ed3a..e78d9c3e3ad86 100644 --- a/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts +++ b/web/packages/teleterm/src/mainProcess/fixtures/mocks.ts @@ -27,7 +27,7 @@ export class MockMainProcessClient implements MainProcessClient { constructor(private runtimeSettings: Partial = {}) { this.configService = createConfigService({ configFile: createMockFileStorage(), - configJsonSchemaFile: createMockFileStorage(), + jsonSchemaFile: createMockFileStorage(), platform: this.getRuntimeSettings().platform, }); } diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index 256694489c289..3862f07678f45 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -206,11 +206,11 @@ function getShortcutDesc(actionDesc: string): string { export function createConfigService({ configFile, - configJsonSchemaFile, + jsonSchemaFile, platform, }: { configFile: FileStorage; - configJsonSchemaFile: FileStorage; + jsonSchemaFile: FileStorage; platform: Platform; }) { const schema = createAppConfigSchema(platform); @@ -218,7 +218,7 @@ export function createConfigService({ updateJsonSchema({ configSchema: schema, configFile, - configJsonSchemaFile, + jsonSchemaFile, }); return createConfigStore(schema, configFile); diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts index 8520b13f08e91..160f842948da1 100644 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts @@ -54,7 +54,7 @@ test('field linking to the schema and the schema itself are updated', () => { updateJsonSchema({ configSchema: schema, configFile: configFile, - configJsonSchemaFile: configJsonSchemaFile, + jsonSchemaFile: configJsonSchemaFile, }); expect(configFile.get('$schema')).toBe('config_schema.json'); diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.ts index 2b8c3ae9236f0..49ddc0f8d3c94 100644 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.ts +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.ts @@ -25,11 +25,11 @@ import { FileStorage } from 'teleterm/services/fileStorage'; export function updateJsonSchema({ configSchema, configFile, - configJsonSchemaFile, + jsonSchemaFile, }: { configSchema: z.AnyZodObject; configFile: FileStorage; - configJsonSchemaFile: FileStorage; + jsonSchemaFile: FileStorage; }): void { //adds $schema field to the original schema to prevent marking it as a not allowed property const configSchemaWithSchemaField = configSchema.extend({ @@ -37,10 +37,10 @@ export function updateJsonSchema({ }); const jsonSchema = zodToJsonSchema(configSchemaWithSchemaField); - configJsonSchemaFile.replace(jsonSchema); + jsonSchemaFile.replace(jsonSchema); linkToJsonSchemaIfNeeded( configFile, - path.basename(configJsonSchemaFile.getFilePath()) + path.basename(jsonSchemaFile.getFilePath()) ); } From 66e6cc383f9cf6d7178fa2613e51b42e8adaaac4 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 10:35:38 +0100 Subject: [PATCH 12/22] Simplify `updateJsonSchema` --- .../src/services/config/configService.ts | 2 +- .../services/config/updateJsonSchema.test.ts | 10 +++---- .../src/services/config/updateJsonSchema.ts | 29 +++++++------------ 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index 3862f07678f45..8d736008b4989 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -216,7 +216,7 @@ export function createConfigService({ const schema = createAppConfigSchema(platform); updateJsonSchema({ - configSchema: schema, + schema, configFile, jsonSchemaFile, }); diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts index 160f842948da1..59e746352f627 100644 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts @@ -47,16 +47,16 @@ const generatedJsonSchema = { test('field linking to the schema and the schema itself are updated', () => { const configFile = createMockFileStorage(); - const configJsonSchemaFile = createMockFileStorage({ + const jsonSchemaFile = createMockFileStorage({ filePath: '~/config_schema.json', }); updateJsonSchema({ - configSchema: schema, - configFile: configFile, - jsonSchemaFile: configJsonSchemaFile, + schema, + configFile, + jsonSchemaFile, }); expect(configFile.get('$schema')).toBe('config_schema.json'); - expect(configJsonSchemaFile.get()).toEqual(generatedJsonSchema); + expect(jsonSchemaFile.get()).toEqual(generatedJsonSchema); }); diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.ts index 49ddc0f8d3c94..d7182a599ac54 100644 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.ts +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.ts @@ -23,33 +23,24 @@ import zodToJsonSchema from 'zod-to-json-schema'; import { FileStorage } from 'teleterm/services/fileStorage'; export function updateJsonSchema({ - configSchema, + schema, configFile, jsonSchemaFile, }: { - configSchema: z.AnyZodObject; + schema: z.AnyZodObject; configFile: FileStorage; jsonSchemaFile: FileStorage; }): void { - //adds $schema field to the original schema to prevent marking it as a not allowed property - const configSchemaWithSchemaField = configSchema.extend({ - $schema: z.string(), - }); - const jsonSchema = zodToJsonSchema(configSchemaWithSchemaField); + const jsonSchema = zodToJsonSchema( + // Add $schema field to prevent marking it as a not allowed property. + schema.extend({ $schema: z.string() }) + ); + const jsonSchemaFileName = path.basename(jsonSchemaFile.getFilePath()); + const jsonSchemaFileNameInConfig = configFile.get('$schema'); jsonSchemaFile.replace(jsonSchema); - linkToJsonSchemaIfNeeded( - configFile, - path.basename(jsonSchemaFile.getFilePath()) - ); -} -function linkToJsonSchemaIfNeeded( - configFileStorage: FileStorage, - jsonSchemaFilename: string -): void { - const schemaField = configFileStorage.get('$schema'); - if (schemaField !== jsonSchemaFilename) { - configFileStorage.put('$schema', jsonSchemaFilename); + if (jsonSchemaFileNameInConfig !== jsonSchemaFileName) { + configFile.put('$schema', jsonSchemaFileName); } } From 3d6a147dda2f0cea43eec33d1f90c33e10d95e86 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 16:34:32 +0100 Subject: [PATCH 13/22] Set `$refStrategy` to none --- web/packages/teleterm/src/services/config/updateJsonSchema.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.ts index d7182a599ac54..b518b9a7805cb 100644 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.ts +++ b/web/packages/teleterm/src/services/config/updateJsonSchema.ts @@ -33,7 +33,8 @@ export function updateJsonSchema({ }): void { const jsonSchema = zodToJsonSchema( // Add $schema field to prevent marking it as a not allowed property. - schema.extend({ $schema: z.string() }) + schema.extend({ $schema: z.string() }), + { $refStrategy: 'none' } ); const jsonSchemaFileName = path.basename(jsonSchemaFile.getFilePath()); const jsonSchemaFileNameInConfig = configFile.get('$schema'); From 26818e6fb67f731c7cd00dcc5f759b09a5c399df Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 16:34:56 +0100 Subject: [PATCH 14/22] Add missing description for `usageReporting.enabled` --- web/packages/teleterm/src/services/config/configService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index 8d736008b4989..2199dcbad8208 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -31,7 +31,10 @@ const createAppConfigSchema = (platform: Platform) => { // `keymap.` prefix is used in `initUi.ts` in a predicate function. return z.object({ - 'usageReporting.enabled': z.boolean().default(false), + 'usageReporting.enabled': z + .boolean() + .default(false) + .describe('Enables collecting of anonymous usage data.'), 'keymap.tab1': shortcutSchema .default(defaultKeymap['tab1']) .describe(getShortcutDesc('open tab 1')), From 6736a36920fe3f3a7e10f23f86d87408dd0f2363 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Mon, 6 Mar 2023 16:35:22 +0100 Subject: [PATCH 15/22] Bump zod to the latest (improves TS performance) --- web/packages/teleterm/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/packages/teleterm/package.json b/web/packages/teleterm/package.json index aabeee9a36fe7..ea053321eb070 100644 --- a/web/packages/teleterm/package.json +++ b/web/packages/teleterm/package.json @@ -56,7 +56,7 @@ "winston": "^3.3.3", "xterm": "^5.0.0", "xterm-addon-fit": "^0.7.0", - "zod": "^3.20.6", + "zod": "^3.21.2", "zod-to-json-schema": "^3.20.4" }, "productName": "Teleport Connect" diff --git a/yarn.lock b/yarn.lock index 9b14d590584b6..1576c844fdfec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14856,10 +14856,10 @@ zod-to-json-schema@^3.20.4: resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.20.4.tgz#155f687c5a059fdc0f1bb3ff32d6e9200036b6f4" integrity sha512-Un9+kInJ2Zt63n6Z7mLqBifzzPcOyX+b+Exuzf7L1+xqck9Q2EPByyTRduV3kmSPaXaRer1JCsucubpgL1fipg== -zod@^3.20.6: - version "3.20.6" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.6.tgz#2f2f08ff81291d47d99e86140fedb4e0db08361a" - integrity sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA== +zod@^3.21.2: + version "3.21.2" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.2.tgz#a25425916d63b74d5ddd0b2a1bf733ecc093964b" + integrity sha512-0Ygy2/IZNIxHterZdHjE5Vb8hp1fUHJD/BGvSHj8QJx+UipEVNvo9WLchoyBpz5JIaN6KmdGDGYdloGzpFK98g== zone.js@^0.11.0: version "0.11.8" From d51f074558c66ab6946855613ec8e152e7f0d7ed Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 7 Mar 2023 09:18:58 +0100 Subject: [PATCH 16/22] Move `configService` implementation to `configStore` --- .../src/services/config/configStore.test.ts | 63 +++++++++--------- .../src/services/config/configStore.ts | 65 +++++++++++-------- ...figService.ts => createAppConfigSchema.ts} | 47 +------------- .../teleterm/src/services/config/index.ts | 1 + 4 files changed, 76 insertions(+), 100 deletions(-) rename web/packages/teleterm/src/services/config/{configService.ts => createAppConfigSchema.ts} (82%) diff --git a/web/packages/teleterm/src/services/config/configStore.test.ts b/web/packages/teleterm/src/services/config/configStore.test.ts index fd892edf947a2..c2c92b2abd3a2 100644 --- a/web/packages/teleterm/src/services/config/configStore.test.ts +++ b/web/packages/teleterm/src/services/config/configStore.test.ts @@ -14,44 +14,45 @@ * limitations under the License. */ -import { z } from 'zod'; - import Logger, { NullService } from 'teleterm/logger'; import { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mocks'; -import { createConfigStore } from './configStore'; +import { createConfigService } from './configStore'; beforeAll(() => { Logger.init(new NullService()); }); -const schema = z.object({ - 'fonts.monoFamily': z.string().default('Arial'), - 'usageReporting.enabled': z.boolean().default(false), -}); - test('stored and default values are combined', () => { - const fileStorage = createMockFileStorage(); - fileStorage.put('usageReporting.enabled', true); - const configStore = createConfigStore(schema, fileStorage); + const configFile = createMockFileStorage(); + configFile.put('usageReporting.enabled', true); + const configService = createConfigService({ + configFile, + jsonSchemaFile: createMockFileStorage(), + platform: 'darwin', + }); - expect(configStore.getStoredConfigErrors()).toBeUndefined(); + expect(configService.getStoredConfigErrors()).toBeUndefined(); - const usageReportingEnabled = configStore.get('usageReporting.enabled'); + const usageReportingEnabled = configService.get('usageReporting.enabled'); expect(usageReportingEnabled.value).toBe(true); expect(usageReportingEnabled.metadata.isStored).toBe(true); - const monoFontFamily = configStore.get('fonts.monoFamily'); - expect(monoFontFamily.value).toBe('Arial'); - expect(monoFontFamily.metadata.isStored).toBe(false); + 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 fileStorage = createMockFileStorage(); - fileStorage.put('usageReporting.enabled', 'abcde'); - const configStore = createConfigStore(schema, fileStorage); - - expect(configStore.getStoredConfigErrors()).toStrictEqual([ + 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', @@ -61,22 +62,26 @@ test('in case of invalid value a default one is returned', () => { }, ]); - const usageReportingEnabled = configStore.get('usageReporting.enabled'); + const usageReportingEnabled = configService.get('usageReporting.enabled'); expect(usageReportingEnabled.value).toBe(false); expect(usageReportingEnabled.metadata.isStored).toBe(false); - const monoFontFamily = configStore.get('fonts.monoFamily'); - expect(monoFontFamily.value).toBe('Arial'); - expect(monoFontFamily.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 fileStorage = createMockFileStorage(); - const configStore = createConfigStore(schema, fileStorage); + const configFile = createMockFileStorage(); + const configService = createConfigService({ + configFile, + jsonSchemaFile: createMockFileStorage(), + platform: 'darwin', + }); - configStore.set('usageReporting.enabled', true); + configService.set('usageReporting.enabled', true); - const usageReportingEnabled = configStore.get('usageReporting.enabled'); + const usageReportingEnabled = configService.get('usageReporting.enabled'); expect(usageReportingEnabled.value).toBe(true); expect(usageReportingEnabled.metadata.isStored).toBe(true); }); diff --git a/web/packages/teleterm/src/services/config/configStore.ts b/web/packages/teleterm/src/services/config/configStore.ts index a10e034465aa1..ebc00739aeabf 100644 --- a/web/packages/teleterm/src/services/config/configStore.ts +++ b/web/packages/teleterm/src/services/config/configStore.ts @@ -14,50 +14,50 @@ * limitations under the License. */ -import { z, ZodIssue } from 'zod'; +import { ZodIssue } from 'zod'; import { FileStorage } from 'teleterm/services/fileStorage'; import Logger from 'teleterm/logger'; +import { Platform } from 'teleterm/mainProcess/types'; -const logger = new Logger('ConfigStore'); +import { createAppConfigSchema, AppConfig } from './createAppConfigSchema'; -export function createConfigStore< - Schema extends z.ZodTypeAny, - Shape = z.infer ->(schema: Schema, fileStorage: FileStorage) { - const { storedConfig, configWithDefaults, errors } = validateStoredConfig(); +const logger = new Logger('ConfigService'); - function get( +export interface ConfigService { + get( key: K - ): { value: Shape[K]; metadata: { isStored: boolean } } { - return { - value: configWithDefaults[key], - metadata: { isStored: storedConfig[key] !== undefined }, - }; - } + ): { value: AppConfig[K]; metadata: { isStored: boolean } }; - function set(key: K, value: Shape[K]): void { - fileStorage.put(key as string, value); - configWithDefaults[key] = value; - storedConfig[key] = value; - } + set(key: K, value: AppConfig[K]): void; - function getStoredConfigErrors(): ZodIssue[] | undefined { - return errors; - } + getStoredConfigErrors(): ZodIssue[] | undefined; +} + +export function createConfigService({ + configFile, + jsonSchemaFile, + platform, +}: { + configFile: FileStorage; + jsonSchemaFile: FileStorage; + platform: Platform; +}): ConfigService { + const schema = createAppConfigSchema(platform); + const { storedConfig, configWithDefaults, errors } = validateStoredConfig(); - function parse(data: Partial) { + function parse(data: Partial) { return schema.safeParse(data); } //TODO (gzdunek): syntax errors of the JSON file are silently ignored, report // them to the user too function validateStoredConfig(): { - storedConfig: Partial; - configWithDefaults: Shape; + storedConfig: Partial; + configWithDefaults: AppConfig; errors: ZodIssue[] | undefined; } { - const storedConfig = fileStorage.get>(); + const storedConfig = configFile.get>(); const parsed = parse(storedConfig); if (parsed.success === true) { return { @@ -88,5 +88,16 @@ export function createConfigStore< }; } - return { get, set, getStoredConfigErrors }; + return { + get: key => ({ + value: configWithDefaults[key], + metadata: { isStored: storedConfig[key] !== undefined }, + }), + set: (key, value) => { + configFile.put(key as string, value); + configWithDefaults[key] = value; + storedConfig[key] = value; + }, + getStoredConfigErrors: () => errors, + }; } diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/createAppConfigSchema.ts similarity index 82% rename from web/packages/teleterm/src/services/config/configService.ts rename to web/packages/teleterm/src/services/config/createAppConfigSchema.ts index 2199dcbad8208..224fafc913bf5 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/createAppConfigSchema.ts @@ -1,29 +1,12 @@ -/** - * 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 { FileStorage } from 'teleterm/services/fileStorage'; import { Platform } from 'teleterm/mainProcess/types'; -import { createConfigStore } from './configStore'; import { getKeyboardShortcutSchema } from './getKeyboardShortcutSchema'; -import { updateJsonSchema } from './updateJsonSchema'; -const createAppConfigSchema = (platform: Platform) => { +export type AppConfig = z.infer>; + +export const createAppConfigSchema = (platform: Platform) => { const defaultKeymap = getDefaultKeymap(platform); const defaultTerminalFont = getDefaultTerminalFont(platform); @@ -106,8 +89,6 @@ const createAppConfigSchema = (platform: Platform) => { }); }; -export type AppConfig = z.infer>; - export type KeyboardShortcutAction = | 'tab1' | 'tab2' @@ -206,25 +187,3 @@ function getDefaultTerminalFont(platform: Platform) { 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.`; } - -export function createConfigService({ - configFile, - jsonSchemaFile, - platform, -}: { - configFile: FileStorage; - jsonSchemaFile: FileStorage; - platform: Platform; -}) { - const schema = createAppConfigSchema(platform); - - updateJsonSchema({ - schema, - configFile, - jsonSchemaFile, - }); - - return createConfigStore(schema, configFile); -} - -export type ConfigService = ReturnType; diff --git a/web/packages/teleterm/src/services/config/index.ts b/web/packages/teleterm/src/services/config/index.ts index 8bc8376c8e463..cf51f792d33c5 100644 --- a/web/packages/teleterm/src/services/config/index.ts +++ b/web/packages/teleterm/src/services/config/index.ts @@ -16,3 +16,4 @@ export * from './configService'; export * from './configServiceClient'; +export { AppConfig, KeyboardShortcutAction } from './createAppConfigSchema'; From 0d5c7c7e3d0700145b8b24fde5f15c20c9db6d56 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 7 Mar 2023 10:02:36 +0100 Subject: [PATCH 17/22] Rename `configStore` to `configService` --- .../config/{configStore.test.ts => configService.test.ts} | 2 +- .../src/services/config/{configStore.ts => configService.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename web/packages/teleterm/src/services/config/{configStore.test.ts => configService.test.ts} (98%) rename web/packages/teleterm/src/services/config/{configStore.ts => configService.ts} (100%) diff --git a/web/packages/teleterm/src/services/config/configStore.test.ts b/web/packages/teleterm/src/services/config/configService.test.ts similarity index 98% rename from web/packages/teleterm/src/services/config/configStore.test.ts rename to web/packages/teleterm/src/services/config/configService.test.ts index c2c92b2abd3a2..9fc5a1eebe397 100644 --- a/web/packages/teleterm/src/services/config/configStore.test.ts +++ b/web/packages/teleterm/src/services/config/configService.test.ts @@ -17,7 +17,7 @@ import Logger, { NullService } from 'teleterm/logger'; import { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mocks'; -import { createConfigService } from './configStore'; +import { createConfigService } from './configService'; beforeAll(() => { Logger.init(new NullService()); diff --git a/web/packages/teleterm/src/services/config/configStore.ts b/web/packages/teleterm/src/services/config/configService.ts similarity index 100% rename from web/packages/teleterm/src/services/config/configStore.ts rename to web/packages/teleterm/src/services/config/configService.ts From 415f3d8c4fe6e568a047744d9f70157d888055bc Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 7 Mar 2023 10:04:06 +0100 Subject: [PATCH 18/22] Move `updateJsonSchema` to `createConfigService` --- .../src/services/config/configService.test.ts | 18 ++++++ .../src/services/config/configService.ts | 31 +++++++++- .../services/config/updateJsonSchema.test.ts | 62 ------------------- .../src/services/config/updateJsonSchema.ts | 47 -------------- 4 files changed, 48 insertions(+), 110 deletions(-) delete mode 100644 web/packages/teleterm/src/services/config/updateJsonSchema.test.ts delete mode 100644 web/packages/teleterm/src/services/config/updateJsonSchema.ts diff --git a/web/packages/teleterm/src/services/config/configService.test.ts b/web/packages/teleterm/src/services/config/configService.test.ts index 9fc5a1eebe397..69e5fe89afc04 100644 --- a/web/packages/teleterm/src/services/config/configService.test.ts +++ b/web/packages/teleterm/src/services/config/configService.test.ts @@ -85,3 +85,21 @@ test('calling set updated the value in store', () => { 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); +}); diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index ebc00739aeabf..d8d37db417d03 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { ZodIssue } from 'zod'; +import path from 'path'; + +import { z, ZodIssue } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; import { FileStorage } from 'teleterm/services/fileStorage'; import Logger from 'teleterm/logger'; @@ -44,6 +47,8 @@ export function createConfigService({ platform: Platform; }): ConfigService { const schema = createAppConfigSchema(platform); + updateJsonSchema({ schema, configFile, jsonSchemaFile }); + const { storedConfig, configWithDefaults, errors } = validateStoredConfig(); function parse(data: Partial) { @@ -101,3 +106,27 @@ export function createConfigService({ getStoredConfigErrors: () => errors, }; } + +function updateJsonSchema({ + schema, + configFile, + jsonSchemaFile, +}: { + schema: z.AnyZodObject; + configFile: FileStorage; + jsonSchemaFile: FileStorage; +}): void { + const jsonSchema = zodToJsonSchema( + // Add $schema field to prevent marking it as a not allowed property. + schema.extend({ $schema: z.string() }), + { $refStrategy: 'none' } + ); + const jsonSchemaFileName = path.basename(jsonSchemaFile.getFilePath()); + const jsonSchemaFileNameInConfig = configFile.get('$schema'); + + jsonSchemaFile.replace(jsonSchema); + + if (jsonSchemaFileNameInConfig !== jsonSchemaFileName) { + configFile.put('$schema', jsonSchemaFileName); + } +} diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts deleted file mode 100644 index 59e746352f627..0000000000000 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 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 { createMockFileStorage } from 'teleterm/services/fileStorage/fixtures/mocks'; - -import { updateJsonSchema } from './updateJsonSchema'; - -const schema = z.object({ - 'fonts.monoFamily': z.string().default('Arial'), - 'usageReporting.enabled': z.boolean().default(false), -}); - -const generatedJsonSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', - additionalProperties: false, - properties: { - $schema: { - type: 'string', - }, - 'fonts.monoFamily': { - default: 'Arial', - type: 'string', - }, - 'usageReporting.enabled': { - default: false, - type: 'boolean', - }, - }, - required: ['$schema'], - type: 'object', -}; - -test('field linking to the schema and the schema itself are updated', () => { - const configFile = createMockFileStorage(); - const jsonSchemaFile = createMockFileStorage({ - filePath: '~/config_schema.json', - }); - - updateJsonSchema({ - schema, - configFile, - jsonSchemaFile, - }); - - expect(configFile.get('$schema')).toBe('config_schema.json'); - expect(jsonSchemaFile.get()).toEqual(generatedJsonSchema); -}); diff --git a/web/packages/teleterm/src/services/config/updateJsonSchema.ts b/web/packages/teleterm/src/services/config/updateJsonSchema.ts deleted file mode 100644 index b518b9a7805cb..0000000000000 --- a/web/packages/teleterm/src/services/config/updateJsonSchema.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * 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 path from 'path'; - -import { z } from 'zod'; - -import zodToJsonSchema from 'zod-to-json-schema'; - -import { FileStorage } from 'teleterm/services/fileStorage'; - -export function updateJsonSchema({ - schema, - configFile, - jsonSchemaFile, -}: { - schema: z.AnyZodObject; - configFile: FileStorage; - jsonSchemaFile: FileStorage; -}): void { - const jsonSchema = zodToJsonSchema( - // Add $schema field to prevent marking it as a not allowed property. - schema.extend({ $schema: z.string() }), - { $refStrategy: 'none' } - ); - const jsonSchemaFileName = path.basename(jsonSchemaFile.getFilePath()); - const jsonSchemaFileNameInConfig = configFile.get('$schema'); - - jsonSchemaFile.replace(jsonSchema); - - if (jsonSchemaFileNameInConfig !== jsonSchemaFileName) { - configFile.put('$schema', jsonSchemaFileName); - } -} From 1380bbf1b17ddfa3d7702aee76f8fe408fa0af0b Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 7 Mar 2023 11:22:51 +0100 Subject: [PATCH 19/22] Move `validateStoredConfig` outside `createConfigService` --- .../src/services/config/configService.ts | 98 ++++++++++--------- .../services/config/createAppConfigSchema.ts | 3 +- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index d8d37db417d03..64644f300b177 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -23,7 +23,11 @@ import { FileStorage } from 'teleterm/services/fileStorage'; import Logger from 'teleterm/logger'; import { Platform } from 'teleterm/mainProcess/types'; -import { createAppConfigSchema, AppConfig } from './createAppConfigSchema'; +import { + createAppConfigSchema, + AppConfigSchema, + AppConfig, +} from './createAppConfigSchema'; const logger = new Logger('ConfigService'); @@ -49,49 +53,10 @@ export function createConfigService({ const schema = createAppConfigSchema(platform); updateJsonSchema({ schema, configFile, jsonSchemaFile }); - const { storedConfig, configWithDefaults, errors } = validateStoredConfig(); - - function parse(data: Partial) { - return schema.safeParse(data); - } - - //TODO (gzdunek): syntax errors of the JSON file are silently ignored, report - // them to the user too - function validateStoredConfig(): { - storedConfig: Partial; - configWithDefaults: AppConfig; - errors: ZodIssue[] | undefined; - } { - const storedConfig = configFile.get>(); - const parsed = parse(storedConfig); - if (parsed.success === true) { - return { - storedConfig, - configWithDefaults: parsed.data, - errors: undefined, - }; - } - const withoutInvalidKeys = { ...storedConfig }; - parsed.error.issues.forEach(error => { - // remove only top-level keys - delete withoutInvalidKeys[error.path[0]]; - logger.info( - `Invalid config key, error: ${error.message} at ${error.path.join('.')}` - ); - }); - const reParsed = parse(withoutInvalidKeys); - if (reParsed.success === false) { - // it can happen when a default value does not pass validation - throw new Error( - `Re-parsing config file failed \n${reParsed.error.message}` - ); - } - return { - storedConfig: withoutInvalidKeys, - configWithDefaults: reParsed.data, - errors: parsed.error.issues, - }; - } + const { storedConfig, configWithDefaults, errors } = validateStoredConfig( + schema, + configFile + ); return { get: key => ({ @@ -112,7 +77,7 @@ function updateJsonSchema({ configFile, jsonSchemaFile, }: { - schema: z.AnyZodObject; + schema: AppConfigSchema; configFile: FileStorage; jsonSchemaFile: FileStorage; }): void { @@ -130,3 +95,46 @@ function updateJsonSchema({ configFile.put('$schema', jsonSchemaFileName); } } + +//TODO (gzdunek): syntax errors of the JSON file are silently ignored, report +// them to the user too +function validateStoredConfig( + schema: AppConfigSchema, + configFile: FileStorage +): { + storedConfig: Partial; + configWithDefaults: AppConfig; + errors: ZodIssue[] | undefined; +} { + const parse = (data: Partial) => schema.safeParse(data); + + const storedConfig = configFile.get>(); + const parsed = parse(storedConfig); + if (parsed.success === true) { + return { + storedConfig, + configWithDefaults: parsed.data, + errors: undefined, + }; + } + const withoutInvalidKeys = { ...storedConfig }; + parsed.error.issues.forEach(error => { + // remove only top-level keys + delete withoutInvalidKeys[error.path[0]]; + logger.info( + `Invalid config key, error: ${error.message} at ${error.path.join('.')}` + ); + }); + const reParsed = parse(withoutInvalidKeys); + if (reParsed.success === false) { + // it can happen when a default value does not pass validation + throw new Error( + `Re-parsing config file failed \n${reParsed.error.message}` + ); + } + return { + storedConfig: withoutInvalidKeys, + configWithDefaults: reParsed.data, + errors: parsed.error.issues, + }; +} diff --git a/web/packages/teleterm/src/services/config/createAppConfigSchema.ts b/web/packages/teleterm/src/services/config/createAppConfigSchema.ts index 224fafc913bf5..cc663389bb194 100644 --- a/web/packages/teleterm/src/services/config/createAppConfigSchema.ts +++ b/web/packages/teleterm/src/services/config/createAppConfigSchema.ts @@ -4,7 +4,8 @@ import { Platform } from 'teleterm/mainProcess/types'; import { getKeyboardShortcutSchema } from './getKeyboardShortcutSchema'; -export type AppConfig = z.infer>; +export type AppConfigSchema = ReturnType; +export type AppConfig = z.infer; export const createAppConfigSchema = (platform: Platform) => { const defaultKeymap = getDefaultKeymap(platform); From d59e2913ebe9791058a31943f3014271dbf72dd3 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 7 Mar 2023 11:27:21 +0100 Subject: [PATCH 20/22] Rename `createAppConfigSchema.ts` to `appConfigSchema.ts`, `getKeyboardShortcutSchema.ts` to `keyboardShortcutSchema.ts` --- .../config/{createAppConfigSchema.ts => appConfigSchema.ts} | 2 +- web/packages/teleterm/src/services/config/configService.ts | 2 +- web/packages/teleterm/src/services/config/index.ts | 2 +- ...ardShortcutSchema.test.ts => keyboardShortcutSchema.test.ts} | 2 +- .../{getKeyboardShortcutSchema.ts => keyboardShortcutSchema.ts} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename web/packages/teleterm/src/services/config/{createAppConfigSchema.ts => appConfigSchema.ts} (98%) rename web/packages/teleterm/src/services/config/{getKeyboardShortcutSchema.test.ts => keyboardShortcutSchema.test.ts} (98%) rename web/packages/teleterm/src/services/config/{getKeyboardShortcutSchema.ts => keyboardShortcutSchema.ts} (100%) diff --git a/web/packages/teleterm/src/services/config/createAppConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts similarity index 98% rename from web/packages/teleterm/src/services/config/createAppConfigSchema.ts rename to web/packages/teleterm/src/services/config/appConfigSchema.ts index cc663389bb194..c4b7c30b0547f 100644 --- a/web/packages/teleterm/src/services/config/createAppConfigSchema.ts +++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { Platform } from 'teleterm/mainProcess/types'; -import { getKeyboardShortcutSchema } from './getKeyboardShortcutSchema'; +import { getKeyboardShortcutSchema } from './keyboardShortcutSchema'; export type AppConfigSchema = ReturnType; export type AppConfig = z.infer; diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index 64644f300b177..7563ef3f95734 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -27,7 +27,7 @@ import { createAppConfigSchema, AppConfigSchema, AppConfig, -} from './createAppConfigSchema'; +} from './appConfigSchema'; const logger = new Logger('ConfigService'); diff --git a/web/packages/teleterm/src/services/config/index.ts b/web/packages/teleterm/src/services/config/index.ts index cf51f792d33c5..9fd174ba63317 100644 --- a/web/packages/teleterm/src/services/config/index.ts +++ b/web/packages/teleterm/src/services/config/index.ts @@ -16,4 +16,4 @@ export * from './configService'; export * from './configServiceClient'; -export { AppConfig, KeyboardShortcutAction } from './createAppConfigSchema'; +export { AppConfig, KeyboardShortcutAction } from './appConfigSchema'; diff --git a/web/packages/teleterm/src/services/config/getKeyboardShortcutSchema.test.ts b/web/packages/teleterm/src/services/config/keyboardShortcutSchema.test.ts similarity index 98% rename from web/packages/teleterm/src/services/config/getKeyboardShortcutSchema.test.ts rename to web/packages/teleterm/src/services/config/keyboardShortcutSchema.test.ts index 61427a95a5a7a..82868f4a4818a 100644 --- a/web/packages/teleterm/src/services/config/getKeyboardShortcutSchema.test.ts +++ b/web/packages/teleterm/src/services/config/keyboardShortcutSchema.test.ts @@ -22,7 +22,7 @@ import { invalidKeyCodeIssue, duplicateModifierIssue, missingModifierIssue, -} from './getKeyboardShortcutSchema'; +} from './keyboardShortcutSchema'; const schema = z.object({ 'keymap.tab1': getKeyboardShortcutSchema('darwin'), diff --git a/web/packages/teleterm/src/services/config/getKeyboardShortcutSchema.ts b/web/packages/teleterm/src/services/config/keyboardShortcutSchema.ts similarity index 100% rename from web/packages/teleterm/src/services/config/getKeyboardShortcutSchema.ts rename to web/packages/teleterm/src/services/config/keyboardShortcutSchema.ts From 60eeb2206dca1ec0ec79275b9e604f6f3b085972 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 7 Mar 2023 11:32:36 +0100 Subject: [PATCH 21/22] Export `createKeyboardShortcutSchema` --- web/packages/teleterm/src/services/config/appConfigSchema.ts | 4 ++-- .../src/services/config/keyboardShortcutSchema.test.ts | 4 ++-- .../teleterm/src/services/config/keyboardShortcutSchema.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/packages/teleterm/src/services/config/appConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts index c4b7c30b0547f..a2e2bd1c64a56 100644 --- a/web/packages/teleterm/src/services/config/appConfigSchema.ts +++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { Platform } from 'teleterm/mainProcess/types'; -import { getKeyboardShortcutSchema } from './keyboardShortcutSchema'; +import { createKeyboardShortcutSchema } from './keyboardShortcutSchema'; export type AppConfigSchema = ReturnType; export type AppConfig = z.infer; @@ -11,7 +11,7 @@ export const createAppConfigSchema = (platform: Platform) => { const defaultKeymap = getDefaultKeymap(platform); const defaultTerminalFont = getDefaultTerminalFont(platform); - const shortcutSchema = getKeyboardShortcutSchema(platform); + const shortcutSchema = createKeyboardShortcutSchema(platform); // `keymap.` prefix is used in `initUi.ts` in a predicate function. return z.object({ diff --git a/web/packages/teleterm/src/services/config/keyboardShortcutSchema.test.ts b/web/packages/teleterm/src/services/config/keyboardShortcutSchema.test.ts index 82868f4a4818a..f2f08aef049f6 100644 --- a/web/packages/teleterm/src/services/config/keyboardShortcutSchema.test.ts +++ b/web/packages/teleterm/src/services/config/keyboardShortcutSchema.test.ts @@ -17,7 +17,7 @@ import { z, ZodError } from 'zod'; import { - getKeyboardShortcutSchema, + createKeyboardShortcutSchema, invalidModifierIssue, invalidKeyCodeIssue, duplicateModifierIssue, @@ -25,7 +25,7 @@ import { } from './keyboardShortcutSchema'; const schema = z.object({ - 'keymap.tab1': getKeyboardShortcutSchema('darwin'), + 'keymap.tab1': createKeyboardShortcutSchema('darwin'), }); function getZodError(...issues: any[]): z.ZodError { diff --git a/web/packages/teleterm/src/services/config/keyboardShortcutSchema.ts b/web/packages/teleterm/src/services/config/keyboardShortcutSchema.ts index 931e147234727..fda66e062ddcb 100644 --- a/web/packages/teleterm/src/services/config/keyboardShortcutSchema.ts +++ b/web/packages/teleterm/src/services/config/keyboardShortcutSchema.ts @@ -55,7 +55,7 @@ export function missingModifierIssue(keyCode: string): z.IssueData { }; } -export function getKeyboardShortcutSchema(platform: Platform) { +export function createKeyboardShortcutSchema(platform: Platform) { const allowedModifiers = getSupportedModifiers(platform); return z From 19a60278081042c32942a8cdfd4cda4b9b9c8f52 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Tue, 7 Mar 2023 11:49:30 +0100 Subject: [PATCH 22/22] Add license header --- .../src/services/config/appConfigSchema.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/web/packages/teleterm/src/services/config/appConfigSchema.ts b/web/packages/teleterm/src/services/config/appConfigSchema.ts index a2e2bd1c64a56..2429561d6eb8d 100644 --- a/web/packages/teleterm/src/services/config/appConfigSchema.ts +++ b/web/packages/teleterm/src/services/config/appConfigSchema.ts @@ -1,3 +1,19 @@ +/** + * 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';