- 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
import process from 'node:process'
import ora from 'ora'
import {
+ ConnectionError,
type ProcedureName,
type RequestPayload,
type ResponsePayload,
import type { Formatter } from '../output/formatter.js'
-import { ConnectionError } from './errors.js'
import { createWsAdapter } from './ws-adapter.js'
const wsFactory: WebSocketFactory = (url, protocols) =>
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))
.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)
})
.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)
})
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')
.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)
})
.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)
})
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')
.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)
})
.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)
})
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')
.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')
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<string, unknown>, {
+ 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')
.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')
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
}
--- /dev/null
+import type { RequestPayload } from 'ui-common'
+
+export const buildHashIdsPayload = (hashIds: string[]): RequestPayload =>
+ hashIds.length > 0 ? { hashIds } : {}
+
+export const pickDefined = (
+ source: Record<string, unknown>,
+ keyMap: Record<string, string>
+): Record<string, unknown> => {
+ const result: Record<string, unknown> = {}
+ for (const [sourceKey, targetKey] of Object.entries(keyMap)) {
+ if (source[sourceKey] != null) {
+ result[targetKey] = source[sourceKey]
+ }
+ }
+ return result
+}
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')
.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)
})
.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)
})
}) => {
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<string, unknown>, {
+ autoStart: 'autoStart',
+ ocppStrict: 'ocppStrictCompliance',
+ persistentConfig: 'persistentConfiguration',
+ supervisionUrl: 'supervisionUrls',
+ }) as RequestPayload,
template: options.template,
}
await runAction(program, ProcedureName.ADD_CHARGING_STATIONS, payload)
.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)
})
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')
.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)
})
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')
const payload: RequestPayload = {
connectorId: options.connectorId,
idTag: options.idTag,
- ...(hashIds.length > 0 && { hashIds }),
+ ...buildHashIdsPayload(hashIds),
}
await runAction(program, ProcedureName.START_TRANSACTION, payload)
})
.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)
})
+++ /dev/null
-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
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
port: DEFAULT_PORT,
protocol: DEFAULT_PROTOCOL,
secure: DEFAULT_SECURE,
- version: DEFAULT_VERSION,
+ version: DEFAULT_PROTOCOL_VERSION,
}
const fileConfig = await loadConfigFile(options.configPath)
-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'
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')
+++ /dev/null
-export const extractErrorMessage = (error: unknown): string =>
- error instanceof Error ? error.message : String(error)
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
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)
})
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 () => {
+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
}
}
}
+
+export const extractErrorMessage = (error: unknown): string =>
+ error instanceof Error ? error.message : String(error)
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'
--- /dev/null
+/* 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;
+}
<template>
- <h1 class="action">
+ <h1 class="action-header">
Add Charging Stations
</h1>
<p>Template:</p>
<br>
<Button
id="action-button"
- @click="
- () => {
- $uiClient
- .addChargingStations(state.template, state.numberOfStations, {
- supervisionUrls: state.supervisionUrl.length > 0 ? state.supervisionUrl : undefined,
- autoStart: convertToBoolean(state.autoStart),
- persistentConfiguration: convertToBoolean(state.persistentConfiguration),
- ocppStrictCompliance: convertToBoolean(state.ocppStrictCompliance),
- enableStatistics: convertToBoolean(state.enableStatistics),
- })
- .then(() => {
- $toast.success('Charging stations successfully added')
- })
- .finally(() => {
- resetToggleButtonState('add-charging-stations', true)
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
- })
- .catch((error: Error) => {
- $toast.error('Error at adding charging stations')
- console.error('Error at adding charging stations:', error)
- })
- }
- "
+ @click="addChargingStations()"
>
Add Charging Stations
</Button>
import type { UUIDv4 } from 'ui-common'
import { ref, watch } from 'vue'
+import { useRouter } from 'vue-router'
import Button from '@/components/buttons/Button.vue'
import {
randomUUID,
resetToggleButtonState,
ROUTE_NAMES,
+ useExecuteAction,
useTemplates,
useUIClient,
} from '@/composables'
})
const $uiClient = useUIClient()
+const $router = useRouter()
const $templates = useTemplates()
+const executeAction = useExecuteAction()
watch($templates, () => {
state.value.renderTemplates = randomUUID()
})
-</script>
-<style scoped>
-.action {
- min-width: max-content;
- color: var(--color-text-strong);
- background-color: var(--color-bg-caption);
- padding: var(--spacing-lg);
+const addChargingStations = (): void => {
+ executeAction(
+ $uiClient.addChargingStations(state.value.template, state.value.numberOfStations, {
+ autoStart: convertToBoolean(state.value.autoStart),
+ enableStatistics: convertToBoolean(state.value.enableStatistics),
+ ocppStrictCompliance: convertToBoolean(state.value.ocppStrictCompliance),
+ persistentConfiguration: convertToBoolean(state.value.persistentConfiguration),
+ supervisionUrls:
+ state.value.supervisionUrl.length > 0 ? state.value.supervisionUrl : undefined,
+ }),
+ 'Charging stations successfully added',
+ 'Error at adding charging stations',
+ () => {
+ resetToggleButtonState('add-charging-stations', true)
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ }
+ )
}
+</script>
+<style scoped>
.number-of-stations {
width: auto;
max-width: 6rem;
<template>
- <h1 class="action">
+ <h1 class="action-header">
Set Supervision Url
</h1>
<h2>{{ chargingStationId }}</h2>
<br>
<Button
id="action-button"
- @click="
- () => {
- $uiClient
- .setSupervisionUrl(hashId, state.supervisionUrl)
- .then(() => {
- $toast.success('Supervision url successfully set')
- })
- .finally(() => {
- resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
- })
- .catch((error: Error) => {
- $toast.error('Error at setting supervision url')
- console.error('Error at setting supervision url:', error)
- })
- }
- "
+ @click="setSupervisionUrl()"
>
Set Supervision Url
</Button>
<script setup lang="ts">
import { ref } from 'vue'
+import { useRouter } from 'vue-router'
import Button from '@/components/buttons/Button.vue'
-import { resetToggleButtonState, ROUTE_NAMES, useUIClient } from '@/composables'
+import { resetToggleButtonState, ROUTE_NAMES, useExecuteAction, useUIClient } from '@/composables'
const props = defineProps<{
chargingStationId: string
})
const $uiClient = useUIClient()
-</script>
+const $router = useRouter()
+const executeAction = useExecuteAction()
-<style scoped>
-.action {
- min-width: max-content;
- color: var(--color-text-strong);
- background-color: var(--color-bg-caption);
- padding: var(--spacing-lg);
+const setSupervisionUrl = (): void => {
+ executeAction(
+ $uiClient.setSupervisionUrl(props.hashId, state.value.supervisionUrl),
+ 'Supervision url successfully set',
+ 'Error at setting supervision url',
+ () => {
+ resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ }
+ )
}
+</script>
+<style scoped>
.supervision-url {
width: 100%;
max-width: 40rem;
<template>
- <h1 class="action">
+ <h1 class="action-header">
Start Transaction
</h1>
<h2>{{ chargingStationId }}</h2>
</script>
<style scoped>
-.action {
- min-width: max-content;
- color: var(--color-text-strong);
- background-color: var(--color-bg-caption);
- padding: var(--spacing-lg);
-}
-
.idtag {
text-align: center;
}
<template>
- <tr class="cs-table__row">
- <td class="cs-table__column">
+ <tr>
+ <td>
{{ chargingStation.stationInfo.chargingStationId }}
</td>
- <td class="cs-table__column">
+ <td>
{{ chargingStation.started === true ? 'Yes' : 'No' }}
</td>
- <td class="cs-table__column">
+ <td>
{{ getSupervisionUrl() }}
</td>
- <td class="cs-table__column">
+ <td>
{{ getWebSocketStateName(chargingStation.wsState) }}
</td>
- <td class="cs-table__column">
+ <td>
{{ chargingStation.bootNotificationResponse?.status ?? EMPTY_VALUE_PLACEHOLDER }}
</td>
- <td class="cs-table__column">
+ <td>
{{ chargingStation.stationInfo.ocppVersion ?? EMPTY_VALUE_PLACEHOLDER }}
</td>
- <td class="cs-table__column">
+ <td>
{{ chargingStation.stationInfo.templateName }}
</td>
- <td class="cs-table__column">
+ <td>
{{ chargingStation.stationInfo.chargePointVendor }}
</td>
- <td class="cs-table__column">
+ <td>
{{ chargingStation.stationInfo.chargePointModel }}
</td>
- <td class="cs-table__column">
+ <td>
{{ chargingStation.stationInfo.firmwareVersion ?? EMPTY_VALUE_PLACEHOLDER }}
</td>
- <td class="cs-table__column">
+ <td>
<StateButton
:active="chargingStation.started === true"
:off="() => stopChargingStation()"
Delete Charging Station
</Button>
</td>
- <td class="cs-table__connectors-column">
- <table class="connectors-table">
- <thead class="connectors-table__head">
- <tr class="connectors-table__row">
- <th
- class="connectors-table__column"
- scope="col"
- >
+ <td class="cs-data__connectors-cell">
+ <table class="data-table">
+ <thead class="data-table__head">
+ <tr>
+ <th scope="col">
Identifier
</th>
- <th
- class="connectors-table__column"
- scope="col"
- >
+ <th scope="col">
Status
</th>
- <th
- class="connectors-table__column"
- scope="col"
- >
+ <th scope="col">
Locked
</th>
- <th
- class="connectors-table__column"
- scope="col"
- >
+ <th scope="col">
Transaction
</th>
- <th
- class="connectors-table__column"
- scope="col"
- >
+ <th scope="col">
ATG Started
</th>
- <th
- class="connectors-table__column"
- scope="col"
- >
+ <th scope="col">
Actions
</th>
</tr>
</thead>
- <tbody class="connectors-table__body">
+ <tbody>
<CSConnector
v-for="entry in getConnectorEntries()"
:key="entry.evseId != null ? `${entry.evseId}-${entry.connectorId}` : entry.connectorId"
</script>
<style scoped>
-.connectors-table {
- width: 100%;
- table-layout: fixed;
- background-color: var(--color-bg-surface);
- border-collapse: collapse;
- empty-cells: show;
-}
-
-.connectors-table__head .connectors-table__row {
- background-color: var(--color-bg-header);
-}
-
-:deep(.connectors-table__row) {
- border: solid 0.25px var(--color-border-row);
-}
-
-:deep(.connectors-table__row:nth-of-type(even)) {
- background-color: var(--color-bg-hover);
-}
-
-:deep(.connectors-table__column) {
- text-align: center;
- vertical-align: middle;
- padding: 0.25rem;
+.cs-data__connectors-cell {
+ vertical-align: top;
+ padding: 0;
}
</style>
<template>
<div class="cs-table__wrapper">
- <table class="cs-table">
- <caption class="cs-table__caption">
+ <table class="data-table data-table--bordered">
+ <caption class="data-table__caption">
Charging Stations
</caption>
<colgroup>
<col>
<col class="cs-table__col--connectors">
</colgroup>
- <thead class="cs-table__head">
- <tr class="cs-table__row">
- <th
- class="cs-table__column"
- scope="col"
- >
+ <thead class="data-table__head">
+ <tr>
+ <th scope="col">
Name
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Started
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Supervision Url
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
WebSocket State
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Registration Status
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
OCPP Version
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Template
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Vendor
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Model
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Firmware
</th>
- <th
- class="cs-table__column"
- scope="col"
- >
+ <th scope="col">
Actions
</th>
<th
</th>
</tr>
</thead>
- <tbody class="cs-table__body">
+ <tbody>
<CSData
v-for="chargingStation in chargingStations"
:key="chargingStation.stationInfo.hashId"
overflow-x: auto;
}
-.cs-table {
- width: 100%;
- table-layout: fixed;
- background-color: var(--color-bg-surface);
- border: solid 0.25px var(--color-border);
- border-collapse: collapse;
- empty-cells: show;
-}
-
-.cs-table__caption {
- color: var(--color-text-strong);
- background-color: var(--color-bg-caption);
- font-size: 1.5rem;
- font-weight: bold;
- padding: 0.5rem;
-}
-
-.cs-table__head .cs-table__row {
- background-color: var(--color-bg-header);
-}
-
-:deep(.cs-table__row) {
- border: solid 0.25px var(--color-border-row);
-}
-
-:deep(.cs-table__row:nth-of-type(even)) {
- background-color: var(--color-bg-hover);
-}
-
-:deep(.cs-table__column) {
- text-align: center;
- vertical-align: middle;
- padding: 0.25rem;
- overflow-wrap: break-word;
-}
-
.cs-table__col--connectors {
width: 33%;
}
-:deep(.cs-table__connectors-column) {
+.cs-table__connectors-column {
vertical-align: top;
padding: 0;
}
-import type { ChargingStationData, ConfigurationData, UUIDv4 } from 'ui-common'
+import type { ChargingStationData, ConfigurationData } from 'ui-common'
import type { InjectionKey, Ref } from 'vue'
+import { randomUUID, validateUUID } from 'ui-common'
import { inject } from 'vue'
import { useToast } from 'vue-toast-notification'
deleteFromLocalStorage(key)
}
-export const randomUUID = (): UUIDv4 => {
- return crypto.randomUUID() as UUIDv4
-}
-
-export const validateUUID = (uuid: unknown): uuid is UUIDv4 => {
- if (typeof uuid !== 'string') {
- return false
- }
- return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(
- uuid
- )
-}
+export { randomUUID, validateUUID }
export const useUIClient = (): UIClient => {
const injected = inject(uiClientKey, undefined)
throw new Error('templates not provided')
}
-export const useExecuteAction = (emit: (event: 'need-refresh') => void) => {
+export const useExecuteAction = (emit?: (event: 'need-refresh') => void) => {
const $toast = useToast()
- return (action: Promise<unknown>, successMsg: string, errorMsg: string): void => {
+ return (
+ action: Promise<unknown>,
+ successMsg: string,
+ errorMsg: string,
+ onFinally?: () => void
+ ): void => {
action
.then(() => {
- emit('need-refresh')
+ emit?.('need-refresh')
return $toast.success(successMsg)
})
+ .finally(onFinally)
.catch((error: unknown) => {
$toast.error(errorMsg)
console.error(`${errorMsg}:`, error)
import 'vue-toast-notification/dist/theme-bootstrap.css'
+import './assets/shared.css'
+
const DEFAULT_THEME = 'tokyo-night-storm'
const loadTheme = async (theme: string): Promise<void> => {
import { toastMock } from '../setup'
import { ButtonStub, createMockUIClient, type MockUIClient } from './helpers'
+vi.mock('vue-router', async importOriginal => {
+ const actual: Record<string, unknown> = await importOriginal()
+ return {
+ ...actual,
+ useRouter: vi.fn(),
+ }
+})
+
+import { useRouter } from 'vue-router'
+
describe('AddChargingStations', () => {
let mockClient: MockUIClient
let mockRouter: { push: ReturnType<typeof vi.fn> }
- /**
- * Mounts AddChargingStations with mock UIClient, router, and templates.
- * @returns Mounted component wrapper
- */
+ /** @returns Mounted component wrapper */
function mountComponent () {
mockClient = createMockUIClient()
mockRouter = { push: vi.fn() }
+ vi.mocked(useRouter).mockReturnValue(mockRouter as unknown as ReturnType<typeof useRouter>)
return mount(AddChargingStations, {
global: {
- config: {
- globalProperties: {
- $router: mockRouter,
- $toast: toastMock,
- } as never,
- },
provide: {
[templatesKey as symbol]: ref(['template-A.json', 'template-B.json']),
[uiClientKey as symbol]: mockClient,
import { TEST_HASH_ID, TEST_STATION_ID } from './constants'
import { ButtonStub, createMockUIClient, type MockUIClient } from './helpers'
+vi.mock('vue-router', async importOriginal => {
+ const actual: Record<string, unknown> = await importOriginal()
+ return {
+ ...actual,
+ useRouter: vi.fn(),
+ }
+})
+
+import { useRouter } from 'vue-router'
+
describe('SetSupervisionUrl', () => {
let mockClient: MockUIClient
let mockRouter: { push: ReturnType<typeof vi.fn> }
/**
- * Mounts SetSupervisionUrl with mock UIClient, router, and toast.
* @param props - Props to override defaults
* @returns Mounted component wrapper
*/
function mountComponent (props = {}) {
mockClient = createMockUIClient()
mockRouter = { push: vi.fn() }
+ vi.mocked(useRouter).mockReturnValue(mockRouter as unknown as ReturnType<typeof useRouter>)
return mount(SetSupervisionUrl, {
global: {
- config: {
- globalProperties: {
- $router: mockRouter,
- $toast: toastMock,
- } as never,
- },
provide: {
[uiClientKey as symbol]: mockClient,
},