From: Jérôme Benoit Date: Fri, 17 Apr 2026 00:29:14 +0000 (+0200) Subject: refactor(ui): factorize shared code across ui packages X-Git-Tag: cli@v4.5.0~51 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=c91d737117e050b08d68f25ae9b3217a574e6188;p=e-mobility-charging-stations-simulator.git refactor(ui): factorize shared code across ui packages - Centralize ConnectionError, extractErrorMessage, and protocol defaults in ui-common - Remove UUID duplication from ui-web (import from ui-common instead) - Extract shared CSS (data-table, action-header) into shared.css - Add buildHashIdsPayload() and pickDefined() helpers to eliminate CLI command duplication - Replace 12 repetitive OCPP command definitions with declarative registry - Extend useExecuteAction() composable with onFinally support and migrate action components - Remove backward-compat re-export shims; consumers import directly from ui-common --- diff --git a/ui/cli/src/client/lifecycle.ts b/ui/cli/src/client/lifecycle.ts index b4e3d7bb..07546ae7 100644 --- a/ui/cli/src/client/lifecycle.ts +++ b/ui/cli/src/client/lifecycle.ts @@ -1,6 +1,7 @@ import process from 'node:process' import ora from 'ora' import { + ConnectionError, type ProcedureName, type RequestPayload, type ResponsePayload, @@ -13,7 +14,6 @@ import { WebSocket as WsWebSocket } from 'ws' import type { Formatter } from '../output/formatter.js' -import { ConnectionError } from './errors.js' import { createWsAdapter } from './ws-adapter.js' const wsFactory: WebSocketFactory = (url, protocols) => diff --git a/ui/cli/src/commands/atg.ts b/ui/cli/src/commands/atg.ts index 290d1827..0bac889f 100644 --- a/ui/cli/src/commands/atg.ts +++ b/ui/cli/src/commands/atg.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { ProcedureName, type RequestPayload } from 'ui-common' import { runAction } from './action.js' +import { buildHashIdsPayload } from './payload.js' const parseCommaSeparatedInts = (value: string): number[] => { const parsed = value.split(',').map(s => Number.parseInt(s.trim(), 10)) @@ -21,7 +22,7 @@ export const createAtgCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { connectorIds?: number[] }) => { const payload: RequestPayload = { ...(options.connectorIds != null && { connectorIds: options.connectorIds }), - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, payload) }) @@ -33,7 +34,7 @@ export const createAtgCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { connectorIds?: number[] }) => { const payload: RequestPayload = { ...(options.connectorIds != null && { connectorIds: options.connectorIds }), - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, payload) }) diff --git a/ui/cli/src/commands/connection.ts b/ui/cli/src/commands/connection.ts index e201c059..fcf6c538 100644 --- a/ui/cli/src/commands/connection.ts +++ b/ui/cli/src/commands/connection.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { ProcedureName, type RequestPayload } from 'ui-common' import { runAction } from './action.js' +import { buildHashIdsPayload } from './payload.js' export const createConnectionCommands = (program: Command): Command => { const cmd = new Command('connection').description('WebSocket connection management') @@ -10,7 +11,7 @@ export const createConnectionCommands = (program: Command): Command => { .command('open [hashIds...]') .description('Open WebSocket connection') .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} + const payload: RequestPayload = buildHashIdsPayload(hashIds) await runAction(program, ProcedureName.OPEN_CONNECTION, payload) }) @@ -18,7 +19,7 @@ export const createConnectionCommands = (program: Command): Command => { .command('close [hashIds...]') .description('Close WebSocket connection') .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} + const payload: RequestPayload = buildHashIdsPayload(hashIds) await runAction(program, ProcedureName.CLOSE_CONNECTION, payload) }) diff --git a/ui/cli/src/commands/connector.ts b/ui/cli/src/commands/connector.ts index 902277f0..25ffadf9 100644 --- a/ui/cli/src/commands/connector.ts +++ b/ui/cli/src/commands/connector.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { ProcedureName, type RequestPayload } from 'ui-common' import { parseInteger, runAction } from './action.js' +import { buildHashIdsPayload } from './payload.js' export const createConnectorCommands = (program: Command): Command => { const cmd = new Command('connector').description('Connector management') @@ -13,7 +14,7 @@ export const createConnectorCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { connectorId: number }) => { const payload: RequestPayload = { connectorId: options.connectorId, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.LOCK_CONNECTOR, payload) }) @@ -25,7 +26,7 @@ export const createConnectorCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { connectorId: number }) => { const payload: RequestPayload = { connectorId: options.connectorId, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.UNLOCK_CONNECTOR, payload) }) diff --git a/ui/cli/src/commands/ocpp.ts b/ui/cli/src/commands/ocpp.ts index 3c4e2267..2536d482 100644 --- a/ui/cli/src/commands/ocpp.ts +++ b/ui/cli/src/commands/ocpp.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { ProcedureName, type RequestPayload } from 'ui-common' import { parseInteger, runAction } from './action.js' +import { buildHashIdsPayload, pickDefined } from './payload.js' export const createOcppCommands = (program: Command): Command => { const cmd = new Command('ocpp').description('OCPP protocol commands') @@ -13,19 +14,11 @@ export const createOcppCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { idTag: string }) => { const payload: RequestPayload = { idTag: options.idTag, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.AUTHORIZE, payload) }) - cmd - .command('boot-notification [hashIds...]') - .description('Send OCPP BootNotification') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.BOOT_NOTIFICATION, payload) - }) - cmd .command('data-transfer [hashIds...]') .description('Send OCPP DataTransfer') @@ -38,63 +31,17 @@ export const createOcppCommands = (program: Command): Command => { options: { data?: string; messageId?: string; vendorId?: string } ) => { const payload: RequestPayload = { - ...(options.vendorId != null && { vendorId: options.vendorId }), - ...(options.messageId != null && { messageId: options.messageId }), - ...(options.data != null && { data: options.data }), - ...(hashIds.length > 0 && { hashIds }), - } + ...pickDefined(options as Record, { + data: 'data', + messageId: 'messageId', + vendorId: 'vendorId', + }), + ...buildHashIdsPayload(hashIds), + } as RequestPayload await runAction(program, ProcedureName.DATA_TRANSFER, payload) } ) - cmd - .command('diagnostics-status-notification [hashIds...]') - .description('Send OCPP DiagnosticsStatusNotification') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION, payload) - }) - - cmd - .command('firmware-status-notification [hashIds...]') - .description('Send OCPP FirmwareStatusNotification') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.FIRMWARE_STATUS_NOTIFICATION, payload) - }) - - cmd - .command('get-15118-ev-certificate [hashIds...]') - .description('Send OCPP Get15118EVCertificate') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.GET_15118_EV_CERTIFICATE, payload) - }) - - cmd - .command('get-certificate-status [hashIds...]') - .description('Send OCPP GetCertificateStatus') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.GET_CERTIFICATE_STATUS, payload) - }) - - cmd - .command('heartbeat [hashIds...]') - .description('Send OCPP Heartbeat') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.HEARTBEAT, payload) - }) - - cmd - .command('log-status-notification [hashIds...]') - .description('Send OCPP LogStatusNotification') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.LOG_STATUS_NOTIFICATION, payload) - }) - cmd .command('meter-values [hashIds...]') .description('Send OCPP MeterValues') @@ -102,43 +49,11 @@ export const createOcppCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { connectorId: number }) => { const payload: RequestPayload = { connectorId: options.connectorId, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.METER_VALUES, payload) }) - cmd - .command('notify-customer-information [hashIds...]') - .description('Send OCPP NotifyCustomerInformation') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.NOTIFY_CUSTOMER_INFORMATION, payload) - }) - - cmd - .command('notify-report [hashIds...]') - .description('Send OCPP NotifyReport') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.NOTIFY_REPORT, payload) - }) - - cmd - .command('security-event-notification [hashIds...]') - .description('Send OCPP SecurityEventNotification') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.SECURITY_EVENT_NOTIFICATION, payload) - }) - - cmd - .command('sign-certificate [hashIds...]') - .description('Send OCPP SignCertificate') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.SIGN_CERTIFICATE, payload) - }) - cmd .command('status-notification [hashIds...]') .description('Send OCPP StatusNotification') @@ -154,19 +69,35 @@ export const createOcppCommands = (program: Command): Command => { connectorId: options.connectorId, errorCode: options.errorCode, status: options.status, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.STATUS_NOTIFICATION, payload) } ) - cmd - .command('transaction-event [hashIds...]') - .description('Send OCPP TransactionEvent') - .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} - await runAction(program, ProcedureName.TRANSACTION_EVENT, payload) - }) + const simpleOcppCommands: [string, string, ProcedureName][] = [ + ['boot-notification', 'Send OCPP BootNotification', ProcedureName.BOOT_NOTIFICATION], + ['diagnostics-status-notification', 'Send OCPP DiagnosticsStatusNotification', ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION], + ['firmware-status-notification', 'Send OCPP FirmwareStatusNotification', ProcedureName.FIRMWARE_STATUS_NOTIFICATION], + ['get-15118-ev-certificate', 'Send OCPP Get15118EVCertificate', ProcedureName.GET_15118_EV_CERTIFICATE], + ['get-certificate-status', 'Send OCPP GetCertificateStatus', ProcedureName.GET_CERTIFICATE_STATUS], + ['heartbeat', 'Send OCPP Heartbeat', ProcedureName.HEARTBEAT], + ['log-status-notification', 'Send OCPP LogStatusNotification', ProcedureName.LOG_STATUS_NOTIFICATION], + ['notify-customer-information', 'Send OCPP NotifyCustomerInformation', ProcedureName.NOTIFY_CUSTOMER_INFORMATION], + ['notify-report', 'Send OCPP NotifyReport', ProcedureName.NOTIFY_REPORT], + ['security-event-notification', 'Send OCPP SecurityEventNotification', ProcedureName.SECURITY_EVENT_NOTIFICATION], + ['sign-certificate', 'Send OCPP SignCertificate', ProcedureName.SIGN_CERTIFICATE], + ['transaction-event', 'Send OCPP TransactionEvent', ProcedureName.TRANSACTION_EVENT], + ] + + for (const [name, description, procedureName] of simpleOcppCommands) { + cmd + .command(`${name} [hashIds...]`) + .description(description) + .action(async (hashIds: string[]) => { + await runAction(program, procedureName, buildHashIdsPayload(hashIds)) + }) + } return cmd } diff --git a/ui/cli/src/commands/payload.ts b/ui/cli/src/commands/payload.ts new file mode 100644 index 00000000..da28585d --- /dev/null +++ b/ui/cli/src/commands/payload.ts @@ -0,0 +1,17 @@ +import type { RequestPayload } from 'ui-common' + +export const buildHashIdsPayload = (hashIds: string[]): RequestPayload => + hashIds.length > 0 ? { hashIds } : {} + +export const pickDefined = ( + source: Record, + keyMap: Record +): Record => { + const result: Record = {} + for (const [sourceKey, targetKey] of Object.entries(keyMap)) { + if (source[sourceKey] != null) { + result[targetKey] = source[sourceKey] + } + } + return result +} diff --git a/ui/cli/src/commands/station.ts b/ui/cli/src/commands/station.ts index 33b75a29..8e082b8a 100644 --- a/ui/cli/src/commands/station.ts +++ b/ui/cli/src/commands/station.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { ProcedureName, type RequestPayload } from 'ui-common' import { parseInteger, runAction } from './action.js' +import { buildHashIdsPayload, pickDefined } from './payload.js' export const createStationCommands = (program: Command): Command => { const cmd = new Command('station').description('Charging station management') @@ -17,7 +18,7 @@ export const createStationCommands = (program: Command): Command => { .command('start [hashIds...]') .description('Start charging station(s)') .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} + const payload: RequestPayload = buildHashIdsPayload(hashIds) await runAction(program, ProcedureName.START_CHARGING_STATION, payload) }) @@ -25,7 +26,7 @@ export const createStationCommands = (program: Command): Command => { .command('stop [hashIds...]') .description('Stop charging station(s)') .action(async (hashIds: string[]) => { - const payload: RequestPayload = hashIds.length > 0 ? { hashIds } : {} + const payload: RequestPayload = buildHashIdsPayload(hashIds) await runAction(program, ProcedureName.STOP_CHARGING_STATION, payload) }) @@ -49,18 +50,12 @@ export const createStationCommands = (program: Command): Command => { }) => { const payload: RequestPayload = { numberOfStations: options.count, - options: { - ...(options.autoStart != null && { autoStart: options.autoStart }), - ...(options.ocppStrict != null && { - ocppStrictCompliance: options.ocppStrict, - }), - ...(options.persistentConfig != null && { - persistentConfiguration: options.persistentConfig, - }), - ...(options.supervisionUrl != null && { - supervisionUrls: options.supervisionUrl, - }), - }, + options: pickDefined(options as Record, { + autoStart: 'autoStart', + ocppStrict: 'ocppStrictCompliance', + persistentConfig: 'persistentConfiguration', + supervisionUrl: 'supervisionUrls', + }) as RequestPayload, template: options.template, } await runAction(program, ProcedureName.ADD_CHARGING_STATIONS, payload) @@ -74,7 +69,7 @@ export const createStationCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { deleteConfig?: true }) => { const payload: RequestPayload = { ...(options.deleteConfig != null && { deleteConfiguration: options.deleteConfig }), - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.DELETE_CHARGING_STATIONS, payload) }) diff --git a/ui/cli/src/commands/supervision.ts b/ui/cli/src/commands/supervision.ts index 36db9407..782181ec 100644 --- a/ui/cli/src/commands/supervision.ts +++ b/ui/cli/src/commands/supervision.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { ProcedureName, type RequestPayload } from 'ui-common' import { runAction } from './action.js' +import { buildHashIdsPayload } from './payload.js' export const createSupervisionCommands = (program: Command): Command => { const cmd = new Command('supervision').description('Supervision URL management') @@ -13,7 +14,7 @@ export const createSupervisionCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { url: string }) => { const payload: RequestPayload = { url: options.url, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.SET_SUPERVISION_URL, payload) }) diff --git a/ui/cli/src/commands/transaction.ts b/ui/cli/src/commands/transaction.ts index 67a1b8bd..59bfdae0 100644 --- a/ui/cli/src/commands/transaction.ts +++ b/ui/cli/src/commands/transaction.ts @@ -2,6 +2,7 @@ import { Command } from 'commander' import { ProcedureName, type RequestPayload } from 'ui-common' import { parseInteger, runAction } from './action.js' +import { buildHashIdsPayload } from './payload.js' export const createTransactionCommands = (program: Command): Command => { const cmd = new Command('transaction').description('Transaction management') @@ -15,7 +16,7 @@ export const createTransactionCommands = (program: Command): Command => { const payload: RequestPayload = { connectorId: options.connectorId, idTag: options.idTag, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.START_TRANSACTION, payload) }) @@ -27,7 +28,7 @@ export const createTransactionCommands = (program: Command): Command => { .action(async (hashIds: string[], options: { transactionId: number }) => { const payload: RequestPayload = { transactionId: options.transactionId, - ...(hashIds.length > 0 && { hashIds }), + ...buildHashIdsPayload(hashIds), } await runAction(program, ProcedureName.STOP_TRANSACTION, payload) }) diff --git a/ui/cli/src/config/defaults.ts b/ui/cli/src/config/defaults.ts deleted file mode 100644 index 90f5114d..00000000 --- a/ui/cli/src/config/defaults.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Protocol, ProtocolVersion } from 'ui-common' - -export const DEFAULT_PROTOCOL = Protocol.UI -export const DEFAULT_VERSION = ProtocolVersion['0.0.1'] -export const DEFAULT_SECURE = false diff --git a/ui/cli/src/config/loader.ts b/ui/cli/src/config/loader.ts index 56d9e59a..135f2156 100644 --- a/ui/cli/src/config/loader.ts +++ b/ui/cli/src/config/loader.ts @@ -5,13 +5,14 @@ import process from 'node:process' import { DEFAULT_HOST, DEFAULT_PORT, + DEFAULT_PROTOCOL, + DEFAULT_PROTOCOL_VERSION, + DEFAULT_SECURE, + extractErrorMessage, uiServerConfigSchema, type UIServerConfigurationSection, } from 'ui-common' -import { extractErrorMessage } from '../utils/errors.js' -import { DEFAULT_PROTOCOL, DEFAULT_SECURE, DEFAULT_VERSION } from './defaults.js' - interface LoadConfigOptions { configPath?: string url?: string @@ -86,7 +87,7 @@ export const loadConfig = async ( port: DEFAULT_PORT, protocol: DEFAULT_PROTOCOL, secure: DEFAULT_SECURE, - version: DEFAULT_VERSION, + version: DEFAULT_PROTOCOL_VERSION, } const fileConfig = await loadConfigFile(options.configPath) diff --git a/ui/cli/src/output/formatter.ts b/ui/cli/src/output/formatter.ts index d3ae953a..8e8f21b5 100644 --- a/ui/cli/src/output/formatter.ts +++ b/ui/cli/src/output/formatter.ts @@ -1,6 +1,5 @@ -import type { ResponsePayload } from 'ui-common' +import { extractErrorMessage, type ResponsePayload } from 'ui-common' -import { extractErrorMessage } from '../utils/errors.js' import { printError } from './human.js' import { outputJson, outputJsonError } from './json.js' import { outputTable } from './table.js' diff --git a/ui/cli/src/output/json.ts b/ui/cli/src/output/json.ts index 9287e761..28c6a97d 100644 --- a/ui/cli/src/output/json.ts +++ b/ui/cli/src/output/json.ts @@ -1,7 +1,5 @@ import process from 'node:process' -import { type ResponsePayload, ResponseStatus } from 'ui-common' - -import { extractErrorMessage } from '../utils/errors.js' +import { extractErrorMessage, type ResponsePayload, ResponseStatus } from 'ui-common' export const outputJson = (payload: ResponsePayload): void => { process.stdout.write(JSON.stringify(payload, null, 2) + '\n') diff --git a/ui/cli/src/utils/errors.ts b/ui/cli/src/utils/errors.ts deleted file mode 100644 index d3e998cd..00000000 --- a/ui/cli/src/utils/errors.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const extractErrorMessage = (error: unknown): string => - error instanceof Error ? error.message : String(error) diff --git a/ui/cli/tests/config.test.ts b/ui/cli/tests/config.test.ts index 23026360..9bff41f2 100644 --- a/ui/cli/tests/config.test.ts +++ b/ui/cli/tests/config.test.ts @@ -3,9 +3,8 @@ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, it } from 'node:test' -import { DEFAULT_HOST, DEFAULT_PORT } from 'ui-common' +import { DEFAULT_HOST, DEFAULT_PORT, DEFAULT_PROTOCOL, DEFAULT_PROTOCOL_VERSION, DEFAULT_SECURE } from 'ui-common' -import { DEFAULT_PROTOCOL, DEFAULT_SECURE, DEFAULT_VERSION } from '../src/config/defaults.js' import { loadConfig } from '../src/config/loader.js' let tempDir: string @@ -32,7 +31,7 @@ await describe('CLI config loader', async () => { assert.strictEqual(config.host, DEFAULT_HOST) assert.strictEqual(config.port, DEFAULT_PORT) assert.strictEqual(config.protocol, DEFAULT_PROTOCOL) - assert.strictEqual(config.version, DEFAULT_VERSION) + assert.strictEqual(config.version, DEFAULT_PROTOCOL_VERSION) assert.strictEqual(config.secure, DEFAULT_SECURE) }) diff --git a/ui/cli/tests/lifecycle.test.ts b/ui/cli/tests/lifecycle.test.ts index c6d54a60..3b6e21a7 100644 --- a/ui/cli/tests/lifecycle.test.ts +++ b/ui/cli/tests/lifecycle.test.ts @@ -2,9 +2,8 @@ import assert from 'node:assert' import { describe, it } from 'node:test' -import { Protocol, ProtocolVersion } from 'ui-common' +import { ConnectionError, Protocol, ProtocolVersion } from 'ui-common' -import { ConnectionError } from '../src/client/errors.js' import { executeCommand } from '../src/client/lifecycle.js' await describe('lifecycle', async () => { diff --git a/ui/common/src/constants.ts b/ui/common/src/constants.ts index 2c3a3b98..b733b07a 100644 --- a/ui/common/src/constants.ts +++ b/ui/common/src/constants.ts @@ -1,3 +1,8 @@ +import { Protocol, ProtocolVersion } from './types/UIProtocol.js' + export const DEFAULT_HOST = 'localhost' export const DEFAULT_PORT = 8080 +export const DEFAULT_PROTOCOL = Protocol.UI +export const DEFAULT_PROTOCOL_VERSION = ProtocolVersion['0.0.1'] +export const DEFAULT_SECURE = false export const UI_WEBSOCKET_REQUEST_TIMEOUT_MS = 60_000 diff --git a/ui/cli/src/client/errors.ts b/ui/common/src/errors.ts similarity index 76% rename from ui/cli/src/client/errors.ts rename to ui/common/src/errors.ts index 80b1fbea..64a9e261 100644 --- a/ui/cli/src/client/errors.ts +++ b/ui/common/src/errors.ts @@ -11,3 +11,6 @@ export class ConnectionError extends Error { } } } + +export const extractErrorMessage = (error: unknown): string => + error instanceof Error ? error.message : String(error) diff --git a/ui/common/src/index.ts b/ui/common/src/index.ts index d50d4026..c18620e2 100644 --- a/ui/common/src/index.ts +++ b/ui/common/src/index.ts @@ -4,6 +4,7 @@ export * from './client/types.js' export * from './client/WebSocketClient.js' export * from './config/schema.js' export * from './constants.js' +export * from './errors.js' export * from './types/ChargingStationType.js' export * from './types/ConfigurationType.js' export * from './types/JsonType.js' diff --git a/ui/web/src/assets/shared.css b/ui/web/src/assets/shared.css new file mode 100644 index 00000000..65e55b64 --- /dev/null +++ b/ui/web/src/assets/shared.css @@ -0,0 +1,59 @@ +/* Shared component styles */ + +/* ── Data table ────────────────────────────────────────────────────── */ +.data-table { + width: 100%; + table-layout: fixed; + background-color: var(--color-bg-surface); + border-collapse: collapse; + empty-cells: show; +} + +.data-table--bordered { + border: solid 0.25px var(--color-border); +} + +.data-table__caption { + color: var(--color-text-strong); + background-color: var(--color-bg-caption); + font-size: 1.5rem; + font-weight: bold; + padding: 0.5rem; +} + +.data-table__head > tr { + background-color: var(--color-bg-header); +} + +.data-table tr { + border: solid 0.25px var(--color-border-row); +} + +.data-table tr:nth-of-type(even) { + background-color: var(--color-bg-hover); +} + +.data-table th, +.data-table td { + text-align: center; + vertical-align: middle; + padding: 0.25rem; +} + +.data-table td { + overflow-wrap: break-word; +} + +/* ── Action views ──────────────────────────────────────────────────── */ +.action-header { + min-width: max-content; + color: var(--color-text-strong); + background-color: var(--color-bg-caption); + padding: var(--spacing-lg); +} + +/* ── Focus ring ────────────────────────────────────────────────────── */ +.focus-outline:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: -2px; +} diff --git a/ui/web/src/components/actions/AddChargingStations.vue b/ui/web/src/components/actions/AddChargingStations.vue index 816fe2e9..fe820e27 100644 --- a/ui/web/src/components/actions/AddChargingStations.vue +++ b/ui/web/src/components/actions/AddChargingStations.vue @@ -1,5 +1,5 @@