From 36d727e924ca6ca1cc62313efbd3eb26c009bef4 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 24 Jan 2025 15:29:23 +0100 Subject: [PATCH 01/15] feat: load and validate cluster config from file --- .../src/kernel-integration/kernel-worker.ts | 68 +++++++++++-------- .../extension/src/vats/default-cluster.json | 23 +++++++ packages/kernel/src/index.ts | 8 ++- packages/kernel/src/types.ts | 11 ++- packages/test-utils/src/index.ts | 1 + packages/utils/package.json | 2 + packages/utils/src/fetchValidatedJson.test.ts | 42 ++++++++++++ packages/utils/src/fetchValidatedJson.ts | 30 ++++++++ yarn.lock | 2 + 9 files changed, 149 insertions(+), 38 deletions(-) create mode 100644 packages/extension/src/vats/default-cluster.json create mode 100644 packages/utils/src/fetchValidatedJson.test.ts create mode 100644 packages/utils/src/fetchValidatedJson.ts diff --git a/packages/extension/src/kernel-integration/kernel-worker.ts b/packages/extension/src/kernel-integration/kernel-worker.ts index 3779003e5..a23771c54 100644 --- a/packages/extension/src/kernel-integration/kernel-worker.ts +++ b/packages/extension/src/kernel-integration/kernel-worker.ts @@ -1,9 +1,11 @@ +import type { Struct } from '@metamask/superstruct'; +import { assert } from '@metamask/superstruct'; import type { + ClusterConfig, KernelCommand, KernelCommandReply, - ClusterConfig, } from '@ocap/kernel'; -import { isKernelCommand, Kernel } from '@ocap/kernel'; +import { ClusterConfigStruct, isKernelCommand, Kernel } from '@ocap/kernel'; import type { PostMessageTarget } from '@ocap/streams'; import { MessagePortDuplexStream, receiveMessagePort } from '@ocap/streams'; import { makeLogger } from '@ocap/utils'; @@ -13,36 +15,37 @@ import { makeSQLKVStore } from './sqlite-kv-store.js'; import { receiveUiConnections } from './ui-connections.js'; import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; -const bundleHost = 'http://localhost:3000'; // XXX placeholder -const sampleBundle = 'sample-vat.bundle'; -const bundleURL = `${bundleHost}/${sampleBundle}`; - -const defaultSubcluster: ClusterConfig = { - bootstrap: 'alice', - vats: { - alice: { - bundleSpec: bundleURL, - parameters: { - name: 'Alice', - }, - }, - bob: { - bundleSpec: bundleURL, - parameters: { - name: 'Bob', - }, - }, - carol: { - bundleSpec: bundleURL, - parameters: { - name: 'Carol', - }, - }, - }, -}; - const logger = makeLogger('[kernel worker]'); +/** + * Load and validate a cluster configuration file + * + * @param configUrl - Path to the config JSON file + * @param validator - The validator to use to validate the config + * @returns The validated cluster configuration + */ +export async function fetchValidatedJson( + configUrl: string, + validator: Struct, +): Promise { + try { + const response = await fetch(configUrl); + if (!response.ok) { + throw new Error( + `Failed to fetch config: ${response.status} ${response.statusText}`, + ); + } + const config = await response.json(); + logger.info(`Loaded cluster config: ${JSON.stringify(config)}`); + assert(config, validator); + return config; + } catch (error) { + throw new Error( + `Failed to load config from ${configUrl}: ${String(error)}`, + ); + } +} + main().catch(logger.error); /** @@ -72,6 +75,11 @@ async function main(): Promise { ); await kernel.init(); + const defaultSubcluster = await fetchValidatedJson( + new URL('../vats/default-cluster.json', import.meta.url).href, + ClusterConfigStruct, + ); + await Promise.all([ vatWorkerClient.start(), // XXX We are mildly concerned that there's a small chance that a race here diff --git a/packages/extension/src/vats/default-cluster.json b/packages/extension/src/vats/default-cluster.json new file mode 100644 index 000000000..66bc03260 --- /dev/null +++ b/packages/extension/src/vats/default-cluster.json @@ -0,0 +1,23 @@ +{ + "bootstrap": "alice", + "vats": { + "alice": { + "bundleSpec": "http://localhost:3000/sample-vat.bundle", + "parameters": { + "name": "Alice" + } + }, + "bob": { + "bundleSpec": "http://localhost:3000/sample-vat.bundle", + "parameters": { + "name": "Bob" + } + }, + "carol": { + "bundleSpec": "http://localhost:3000/sample-vat.bundle", + "parameters": { + "name": "Carol" + } + } + } +} diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts index 1ba543bb2..87fb5cd27 100644 --- a/packages/kernel/src/index.ts +++ b/packages/kernel/src/index.ts @@ -11,4 +11,10 @@ export type { ClusterConfig, VatConfig, } from './types.js'; -export { isVatId, VatIdStruct, isVatConfig, VatConfigStruct } from './types.js'; +export { + isVatId, + VatIdStruct, + isVatConfig, + VatConfigStruct, + ClusterConfigStruct, +} from './types.js'; diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 0875b985a..c442a21ab 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -13,6 +13,7 @@ import { union, literal, } from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; import { UnsafeJsonStruct } from '@metamask/utils'; import type { DuplexStream } from '@ocap/streams'; @@ -265,18 +266,14 @@ export const isVatConfig = (value: unknown): value is VatConfig => export type VatConfigTable = Record; -export type ClusterConfig = { - bootstrap?: string; - vats: VatConfigTable; - bundles?: VatConfigTable; -}; - export const ClusterConfigStruct = object({ - bootstrap: optional(string()), + bootstrap: string(), vats: record(string(), VatConfigStruct), bundles: optional(record(string(), VatConfigStruct)), }); +export type ClusterConfig = Infer; + export const isClusterConfig = (value: unknown): value is ClusterConfig => is(value, ClusterConfigStruct); diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index cddc2ccad..c31c38ff3 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -1,6 +1,7 @@ export { delay } from './delay.js'; export { makeErrorMatcherFactory } from './errors.js'; export { makePromiseKitMock } from './promise-kit.js'; +export { fetchMock } from './env/fetch-mock.js'; if (typeof self === 'undefined') { // @ts-expect-error error concerns the browser but this will only run in Node diff --git a/packages/utils/package.json b/packages/utils/package.json index c8a5080c2..5cf3ea52c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -53,7 +53,9 @@ "@metamask/eslint-config": "^14.0.0", "@metamask/eslint-config-nodejs": "^14.0.0", "@metamask/eslint-config-typescript": "^14.0.0", + "@metamask/superstruct": "^3.1.0", "@ocap/cli": "workspace:^", + "@ocap/test-utils": "workspace:^", "@ts-bridge/cli": "^0.6.2", "@ts-bridge/shims": "^0.1.1", "@typescript-eslint/eslint-plugin": "^8.8.1", diff --git a/packages/utils/src/fetchValidatedJson.test.ts b/packages/utils/src/fetchValidatedJson.test.ts new file mode 100644 index 000000000..810e7ff46 --- /dev/null +++ b/packages/utils/src/fetchValidatedJson.test.ts @@ -0,0 +1,42 @@ +import { object, string } from '@metamask/superstruct'; +import { fetchMock } from '@ocap/test-utils'; +import { describe, it, expect } from 'vitest'; + +import { fetchValidatedJson } from './fetchValidatedJson.js'; + +describe('fetchValidatedJson', () => { + const TestConfigStruct = object({ + name: string(), + }); + + it('fetches and validates JSON successfully', async () => { + const mockConfig = { + name: 'test', + }; + + fetchMock.mockResponseOnce(JSON.stringify(mockConfig)); + + const result = await fetchValidatedJson( + 'http://test.url', + TestConfigStruct, + ); + expect(result).toStrictEqual(mockConfig); + expect(fetchMock).toHaveBeenCalledWith('http://test.url'); + }); + + it('throws on fetch failure', async () => { + fetchMock.mockResponseOnce('', { status: 404, statusText: 'Not Found' }); + + await expect( + fetchValidatedJson('http://test.url', TestConfigStruct), + ).rejects.toThrow('Failed to fetch config: 404 Not Found'); + }); + + it('throws on invalid JSON', async () => { + fetchMock.mockResponseOnce(JSON.stringify({ invalid: 'config' })); + + await expect( + fetchValidatedJson('http://test.url', TestConfigStruct), + ).rejects.toThrow('Failed to load config from http://test.url'); + }); +}); diff --git a/packages/utils/src/fetchValidatedJson.ts b/packages/utils/src/fetchValidatedJson.ts new file mode 100644 index 000000000..8255846c4 --- /dev/null +++ b/packages/utils/src/fetchValidatedJson.ts @@ -0,0 +1,30 @@ +import type { Struct } from '@metamask/superstruct'; +import { assert } from '@metamask/superstruct'; + +/** + * Load and validate a cluster configuration file + * + * @param configUrl - Path to the config JSON file + * @param validator - The validator to use to validate the config + * @returns The validated cluster configuration + */ +export async function fetchValidatedJson( + configUrl: string, + validator: Struct, +): Promise { + try { + const response = await fetch(configUrl); + if (!response.ok) { + throw new Error( + `Failed to fetch config: ${response.status} ${response.statusText}`, + ); + } + const config = await response.json(); + assert(config, validator); + return config; + } catch (error) { + throw new Error( + `Failed to load config from ${configUrl}: ${String(error)}`, + ); + } +} diff --git a/yarn.lock b/yarn.lock index b0de7f3da..ddaea7254 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2454,9 +2454,11 @@ __metadata: "@metamask/eslint-config": "npm:^14.0.0" "@metamask/eslint-config-nodejs": "npm:^14.0.0" "@metamask/eslint-config-typescript": "npm:^14.0.0" + "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.0.1" "@ocap/cli": "workspace:^" "@ocap/errors": "workspace:^" + "@ocap/test-utils": "workspace:^" "@ts-bridge/cli": "npm:^0.6.2" "@ts-bridge/shims": "npm:^0.1.1" "@typescript-eslint/eslint-plugin": "npm:^8.8.1" From 078b7d94033668724bd62b214bbcba080d76a1e1 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 24 Jan 2025 16:01:48 +0100 Subject: [PATCH 02/15] chore: remove moved method --- .../src/kernel-integration/kernel-worker.ts | 33 +------------------ packages/utils/src/index.ts | 1 + 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/packages/extension/src/kernel-integration/kernel-worker.ts b/packages/extension/src/kernel-integration/kernel-worker.ts index a23771c54..62efcd030 100644 --- a/packages/extension/src/kernel-integration/kernel-worker.ts +++ b/packages/extension/src/kernel-integration/kernel-worker.ts @@ -1,5 +1,3 @@ -import type { Struct } from '@metamask/superstruct'; -import { assert } from '@metamask/superstruct'; import type { ClusterConfig, KernelCommand, @@ -8,7 +6,7 @@ import type { import { ClusterConfigStruct, isKernelCommand, Kernel } from '@ocap/kernel'; import type { PostMessageTarget } from '@ocap/streams'; import { MessagePortDuplexStream, receiveMessagePort } from '@ocap/streams'; -import { makeLogger } from '@ocap/utils'; +import { fetchValidatedJson, makeLogger } from '@ocap/utils'; import { handlePanelMessage } from './handle-panel-message.js'; import { makeSQLKVStore } from './sqlite-kv-store.js'; @@ -17,35 +15,6 @@ import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; const logger = makeLogger('[kernel worker]'); -/** - * Load and validate a cluster configuration file - * - * @param configUrl - Path to the config JSON file - * @param validator - The validator to use to validate the config - * @returns The validated cluster configuration - */ -export async function fetchValidatedJson( - configUrl: string, - validator: Struct, -): Promise { - try { - const response = await fetch(configUrl); - if (!response.ok) { - throw new Error( - `Failed to fetch config: ${response.status} ${response.statusText}`, - ); - } - const config = await response.json(); - logger.info(`Loaded cluster config: ${JSON.stringify(config)}`); - assert(config, validator); - return config; - } catch (error) { - throw new Error( - `Failed to load config from ${configUrl}: ${String(error)}`, - ); - } -} - main().catch(logger.error); /** diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 51645be7e..e095864f3 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,3 +4,4 @@ export { delay, makeCounter } from './misc.js'; export { stringify } from './stringify.js'; export type { TypeGuard, ExtractGuardType } from './types.js'; export { isPrimitive, isTypedArray, isTypedObject } from './types.js'; +export { fetchValidatedJson } from './fetchValidatedJson.js'; From d7a163b048fb729f5c26dc3f88ee6ff2c902d732 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 24 Jan 2025 18:51:00 +0100 Subject: [PATCH 03/15] feat: Edit cluster config from panel and reload kernel --- .../handle-panel-message.test.ts | 26 +++++- .../kernel-integration/handlers/get-status.ts | 3 +- .../src/kernel-integration/handlers/index.ts | 2 + .../handlers/reload-config.ts | 2 +- .../handlers/update-cluster-config.test.ts | 40 +++++++++ .../handlers/update-cluster-config.ts | 23 +++++ .../src/kernel-integration/messages.test.ts | 2 + .../src/kernel-integration/messages.ts | 30 +++++-- packages/extension/src/ui/App.module.css | 31 ++++++- .../src/ui/components/ConfigEditor.tsx | 87 +++++++++++++++++++ .../src/ui/components/KernelControls.test.tsx | 5 +- .../src/ui/components/KernelControls.tsx | 4 +- .../src/ui/components/VatManager.tsx | 2 + .../extension/src/ui/context/PanelContext.tsx | 7 +- .../src/ui/hooks/useKernelActions.ts | 19 +++- .../src/ui/hooks/useStatusPolling.ts | 19 ++-- .../extension/src/vats/default-cluster.json | 1 + packages/kernel/src/Kernel.ts | 33 +++++++ packages/kernel/src/index.test.ts | 1 + packages/kernel/src/types.ts | 2 + 20 files changed, 305 insertions(+), 34 deletions(-) create mode 100644 packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts create mode 100644 packages/extension/src/kernel-integration/handlers/update-cluster-config.ts create mode 100644 packages/extension/src/ui/components/ConfigEditor.tsx diff --git a/packages/extension/src/kernel-integration/handle-panel-message.test.ts b/packages/extension/src/kernel-integration/handle-panel-message.test.ts index acd7a843a..ccf191752 100644 --- a/packages/extension/src/kernel-integration/handle-panel-message.test.ts +++ b/packages/extension/src/kernel-integration/handle-panel-message.test.ts @@ -1,5 +1,13 @@ import '@ocap/test-utils/mock-endoify'; -import { define, literal, object } from '@metamask/superstruct'; +import { + boolean, + define, + literal, + object, + optional, + record, + string, +} from '@metamask/superstruct'; import type { Kernel, KernelCommand, @@ -22,15 +30,24 @@ vi.mock('@ocap/utils', () => ({ let isVatConfigMock = true; let isVatIdMock = true; +const VatIdStruct = define('VatId', () => isVatIdMock); +const VatConfigStruct = define('VatConfig', () => isVatConfigMock); + // Mock kernel validation functions // because vitest needs to extend Error stack and under SES it fails vi.mock('@ocap/kernel', () => ({ isKernelCommand: () => true, isVatId: () => isVatIdMock, isVatConfig: () => isVatConfigMock, - VatIdStruct: define('VatId', () => isVatIdMock), - VatConfigStruct: define('VatConfig', () => isVatConfigMock), - KernelSendVatCommandStruct: object({ + VatIdStruct, + VatConfigStruct, + ClusterConfigStruct: object({ + bootstrap: string(), + forceReset: optional(boolean()), + vats: record(string(), VatConfigStruct), + bundles: optional(record(string(), VatConfigStruct)), + }), + KernelSendMessageStruct: object({ id: literal('v0'), payload: object({ method: literal('ping'), @@ -277,6 +294,7 @@ describe('handlePanelMessage', () => { payload: { method: 'getStatus', params: { + clusterConfig: undefined, vats: [ { id: 'v0', diff --git a/packages/extension/src/kernel-integration/handlers/get-status.ts b/packages/extension/src/kernel-integration/handlers/get-status.ts index b0f1e3a3f..f91e049a8 100644 --- a/packages/extension/src/kernel-integration/handlers/get-status.ts +++ b/packages/extension/src/kernel-integration/handlers/get-status.ts @@ -15,6 +15,7 @@ export const getStatusHandler: CommandHandler = { implementation: async (kernel: Kernel): Promise => { return { vats: kernel.getVats(), - }; + clusterConfig: kernel.clusterConfig, + } as Json; }, }; diff --git a/packages/extension/src/kernel-integration/handlers/index.ts b/packages/extension/src/kernel-integration/handlers/index.ts index e6de62f2d..90cb6953b 100644 --- a/packages/extension/src/kernel-integration/handlers/index.ts +++ b/packages/extension/src/kernel-integration/handlers/index.ts @@ -7,6 +7,7 @@ import { restartVatHandler } from './restart-vat.js'; import { sendVatCommandHandler } from './send-vat-command.js'; import { terminateAllVatsHandler } from './terminate-all-vats.js'; import { terminateVatHandler } from './terminate-vat.js'; +import { updateClusterConfigHandler } from './update-cluster-config.js'; export const handlers = [ getStatusHandler, @@ -18,4 +19,5 @@ export const handlers = [ restartVatHandler, terminateVatHandler, terminateAllVatsHandler, + updateClusterConfigHandler, ] as const; diff --git a/packages/extension/src/kernel-integration/handlers/reload-config.ts b/packages/extension/src/kernel-integration/handlers/reload-config.ts index eaa1a8d2d..d7d3c3e32 100644 --- a/packages/extension/src/kernel-integration/handlers/reload-config.ts +++ b/packages/extension/src/kernel-integration/handlers/reload-config.ts @@ -13,7 +13,7 @@ export const reloadConfigHandler: CommandHandler = { method: KernelControlMethod.reload, schema: KernelCommandPayloadStructs.clearState.schema.params, implementation: async (kernel: Kernel): Promise => { - await kernel.reset(); + await kernel.reload(); return null; }, }; diff --git a/packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts b/packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts new file mode 100644 index 000000000..2a197745c --- /dev/null +++ b/packages/extension/src/kernel-integration/handlers/update-cluster-config.test.ts @@ -0,0 +1,40 @@ +import type { Kernel, KVStore } from '@ocap/kernel'; +import { describe, it, expect } from 'vitest'; + +import { updateClusterConfigHandler } from './update-cluster-config.js'; + +describe('updateClusterConfigHandler', () => { + const mockKernel = { + clusterConfig: null, + } as Partial; + + const mockKvStore = {} as KVStore; + + const testConfig = { + bootstrap: 'test-bootstrap', + vats: { + testVat: { + sourceSpec: 'test-source', + }, + }, + }; + + it('should update kernel cluster config', async () => { + const result = await updateClusterConfigHandler.implementation( + mockKernel as Kernel, + mockKvStore, + { config: testConfig }, + ); + + expect(mockKernel.clusterConfig).toStrictEqual(testConfig); + expect(result).toBeNull(); + }); + + it('should use the correct method name', () => { + expect(updateClusterConfigHandler.method).toBe('updateClusterConfig'); + }); + + it('should validate the config using the correct schema', () => { + expect(updateClusterConfigHandler.schema).toBeDefined(); + }); +}); diff --git a/packages/extension/src/kernel-integration/handlers/update-cluster-config.ts b/packages/extension/src/kernel-integration/handlers/update-cluster-config.ts new file mode 100644 index 000000000..0ec016cb8 --- /dev/null +++ b/packages/extension/src/kernel-integration/handlers/update-cluster-config.ts @@ -0,0 +1,23 @@ +import type { Json } from '@metamask/utils'; +import type { ClusterConfig, Kernel, KVStore } from '@ocap/kernel'; + +import type { CommandHandler } from '../command-registry.js'; +import { + KernelCommandPayloadStructs, + KernelControlMethod, +} from '../messages.js'; + +export const updateClusterConfigHandler: CommandHandler< + typeof KernelControlMethod.updateClusterConfig +> = { + method: KernelControlMethod.updateClusterConfig, + schema: KernelCommandPayloadStructs.updateClusterConfig.schema.params, + implementation: async ( + kernel: Kernel, + _kvStore: KVStore, + params: { config: ClusterConfig }, + ): Promise => { + kernel.clusterConfig = params.config; + return null; + }, +}; diff --git a/packages/extension/src/kernel-integration/messages.test.ts b/packages/extension/src/kernel-integration/messages.test.ts index b2b251f6d..1d58a75c0 100644 --- a/packages/extension/src/kernel-integration/messages.test.ts +++ b/packages/extension/src/kernel-integration/messages.test.ts @@ -20,6 +20,7 @@ describe('KernelControlMethod', () => { 'sendVatCommand', 'clearState', 'executeDBQuery', + 'updateClusterConfig', ]); }); }); @@ -179,6 +180,7 @@ describe('isKernelControlReply', () => { payload: { method: KernelControlMethod.getStatus, params: { + clusterConfig: undefined, vats: [ { id: 'v0', diff --git a/packages/extension/src/kernel-integration/messages.ts b/packages/extension/src/kernel-integration/messages.ts index 99d24a518..acbc1e730 100644 --- a/packages/extension/src/kernel-integration/messages.ts +++ b/packages/extension/src/kernel-integration/messages.ts @@ -11,8 +11,11 @@ import { import type { Infer } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; import { UnsafeJsonStruct } from '@metamask/utils'; -import type { VatConfig, VatId } from '@ocap/kernel'; -import { VatConfigStruct, VatIdStruct } from '@ocap/kernel'; +import { + ClusterConfigStruct, + VatConfigStruct, + VatIdStruct, +} from '@ocap/kernel'; import type { TypeGuard } from '@ocap/utils'; export const KernelControlMethod = { @@ -25,18 +28,13 @@ export const KernelControlMethod = { sendVatCommand: 'sendVatCommand', clearState: 'clearState', executeDBQuery: 'executeDBQuery', + updateClusterConfig: 'updateClusterConfig', } as const; export type KernelMethods = keyof typeof KernelControlMethod; -export type KernelStatus = { - vats: { - id: VatId; - config: VatConfig; - }[]; -}; - const KernelStatusStruct = type({ + clusterConfig: ClusterConfigStruct, vats: array( object({ id: VatIdStruct, @@ -45,6 +43,8 @@ const KernelStatusStruct = type({ ), }); +export type KernelStatus = Infer; + // Command payload structs export const KernelCommandPayloadStructs = { [KernelControlMethod.launchVat]: object({ @@ -88,6 +88,12 @@ export const KernelCommandPayloadStructs = { sql: string(), }), }), + [KernelControlMethod.updateClusterConfig]: object({ + method: literal(KernelControlMethod.updateClusterConfig), + params: object({ + config: ClusterConfigStruct, + }), + }), } as const; export const KernelReplyPayloadStructs = { @@ -130,6 +136,10 @@ export const KernelReplyPayloadStructs = { object({ error: string() }), ]), }), + [KernelControlMethod.updateClusterConfig]: object({ + method: literal(KernelControlMethod.updateClusterConfig), + params: literal(null), + }), } as const; const KernelControlCommandStruct = object({ @@ -144,6 +154,7 @@ const KernelControlCommandStruct = object({ KernelCommandPayloadStructs.sendVatCommand, KernelCommandPayloadStructs.clearState, KernelCommandPayloadStructs.executeDBQuery, + KernelCommandPayloadStructs.updateClusterConfig, ]), }); @@ -159,6 +170,7 @@ const KernelControlReplyStruct = object({ KernelReplyPayloadStructs.sendVatCommand, KernelReplyPayloadStructs.clearState, KernelReplyPayloadStructs.executeDBQuery, + KernelReplyPayloadStructs.updateClusterConfig, ]), }); diff --git a/packages/extension/src/ui/App.module.css b/packages/extension/src/ui/App.module.css index c168cc121..9ace05af3 100644 --- a/packages/extension/src/ui/App.module.css +++ b/packages/extension/src/ui/App.module.css @@ -6,6 +6,7 @@ --color-gray-200: #f0f0f0; --color-gray-300: #ccc; --color-gray-600: #666; + --color-gray-800: #333; --color-primary: #4956f9; --color-success: #4caf50; --color-error: #f44336; @@ -143,11 +144,17 @@ h6 { border: none; } -.button:hover:not(:disabled) { +.buttonBlack { + composes: button; background-color: var(--color-black); color: var(--color-white); + border: none; } +.button:hover:not(:disabled) { + background-color: var(--color-gray-800); + color: var(--color-white); +} .textButton { padding: 0; border: 0; @@ -472,3 +479,25 @@ div + .sent { display: flex; gap: var(--spacing-sm); } + +.configEditor { + margin: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.configTextarea { + font-family: monospace; + padding: var(--spacing-sm); + border: 1px solid var(--color-gray-300); + border-radius: var(--border-radius); + resize: vertical; + min-height: 200px; +} + +.configEditorButtons { + display: flex; + gap: var(--spacing-sm); + justify-content: flex-end; +} diff --git a/packages/extension/src/ui/components/ConfigEditor.tsx b/packages/extension/src/ui/components/ConfigEditor.tsx new file mode 100644 index 000000000..62f998b68 --- /dev/null +++ b/packages/extension/src/ui/components/ConfigEditor.tsx @@ -0,0 +1,87 @@ +import type { ClusterConfig } from '@ocap/kernel'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import type { KernelStatus } from '../../kernel-integration/messages.js'; +import styles from '../App.module.css'; +import { usePanelContext } from '../context/PanelContext.js'; +import { useKernelActions } from '../hooks/useKernelActions.js'; + +/** + * Component for editing the kernel cluster configuration. + * + * @param options - The component options + * @param options.status - The kernel status + * @returns A React component for editing the kernel cluster configuration. + */ +export const ConfigEditorInner: React.FC<{ status: KernelStatus }> = ({ + status, +}) => { + const { updateClusterConfig, reload } = useKernelActions(); + const { logMessage } = usePanelContext(); + const clusterConfig = useMemo( + () => JSON.stringify(status.clusterConfig, null, 2), + [status], + ); + const [config, setConfig] = useState(clusterConfig); + + // Update the config when the status changes + useEffect(() => { + setConfig(clusterConfig); + }, [clusterConfig]); + + const handleUpdate = useCallback( + (reloadKernel = false) => { + try { + const parsedConfig: ClusterConfig = JSON.parse(config); + updateClusterConfig(parsedConfig) + .then(() => reloadKernel && reload()) + .catch((error) => { + logMessage(String(error), 'error'); + }); + } catch (error) { + logMessage(String(error), 'error'); + } + }, + [config, updateClusterConfig], + ); + + if (!config) { + return null; + } + + return ( +
+

Cluster Config

+