@c8y/ngx-components 1024.5.1 → 1024.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{c8y-ngx-components-asset-property-grid.component-DhfJagxU.mjs → c8y-ngx-components-asset-property-grid.component-Ddva2QJW.mjs} +35 -19
- package/fesm2022/c8y-ngx-components-asset-property-grid.component-Ddva2QJW.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs +28 -14
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs +2 -2
- package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-icon-selector.mjs +1538 -59
- package/fesm2022/c8y-ngx-components-icon-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-remote-access-data.mjs +9 -2
- package/fesm2022/c8y-ngx-components-remote-access-data.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-remote-access-terminal-viewer.mjs +6 -1
- package/fesm2022/c8y-ngx-components-remote-access-terminal-viewer.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-repository-configuration.mjs +94 -49
- package/fesm2022/c8y-ngx-components-repository-configuration.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components.mjs +1 -1
- package/locales/de.po +266 -3
- package/locales/es.po +266 -3
- package/locales/fr.po +266 -3
- package/locales/ja_JP.po +265 -3
- package/locales/ko.po +266 -3
- package/locales/locales.pot +266 -3
- package/locales/nl.po +266 -3
- package/locales/pl.po +266 -3
- package/locales/pt_BR.po +266 -3
- package/locales/zh_CN.po +266 -3
- package/locales/zh_TW.po +266 -3
- package/package.json +1 -1
- package/types/c8y-ngx-components-context-dashboard.d.ts +7 -3
- package/types/c8y-ngx-components-context-dashboard.d.ts.map +1 -1
- package/types/c8y-ngx-components-icon-selector.d.ts +286 -32
- package/types/c8y-ngx-components-icon-selector.d.ts.map +1 -1
- package/types/c8y-ngx-components-remote-access-data.d.ts +8 -1
- package/types/c8y-ngx-components-remote-access-data.d.ts.map +1 -1
- package/types/c8y-ngx-components-remote-access-terminal-viewer.d.ts.map +1 -1
- package/types/c8y-ngx-components-repository-configuration.d.ts +20 -10
- package/types/c8y-ngx-components-repository-configuration.d.ts.map +1 -1
- package/fesm2022/c8y-ngx-components-asset-property-grid.component-DhfJagxU.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"c8y-ngx-components-remote-access-data.mjs","sources":["../../remote-access/data/remote-access.service.ts","../../remote-access/data/c8y-ngx-components-remote-access-data.ts"],"sourcesContent":["import { DOCUMENT, inject, Injectable } from '@angular/core';\nimport { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router';\nimport { FetchClient, IManagedObject } from '@c8y/client';\nimport {\n ContextRouteService,\n Permissions,\n ServiceRegistry,\n ViewContext\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { Observable, defer, shareReplay } from 'rxjs';\nimport { RemoteAccessProtocolProvider } from './remote-access-protocol-provider';\nimport { uniqWith, intersectionWith, isEqual } from 'lodash';\n\nexport interface RemoteAccessConfiguration {\n id: string;\n name: string;\n hostname: string;\n port: number;\n protocol: string;\n attrs?: any;\n credentials?: any;\n credentialsType?: string;\n}\n\nexport const CREDENTIALS_TYPES = {\n NONE: {\n name: 'NONE',\n value: 'NONE',\n label: gettext('No password')\n },\n USER_PASS: {\n name: 'USER_PASS',\n value: 'USER_PASS',\n label: gettext('Username and password')\n },\n PASS_ONLY: {\n name: 'PASS_ONLY',\n value: 'PASS_ONLY',\n label: gettext('Password only')\n },\n KEY_PAIR: {\n name: 'KEY_PAIR',\n value: 'KEY_PAIR',\n label: gettext('Public/private keys')\n },\n CERTIFICATE: {\n name: 'CERTIFICATE',\n value: 'CERTIFICATE',\n label: gettext('Certificate')\n }\n} as const;\n\nexport const canActivateRemoteAccess: CanActivateFn = (route: ActivatedRouteSnapshot) => {\n const permissions = inject(Permissions);\n const remoteAccessService = inject(RemoteAccessService);\n const contextRouteService = inject(ContextRouteService);\n if (!permissions.hasRole(Permissions.ROLE_REMOTE_ACCESS_ADMIN)) {\n return false;\n }\n\n const contextDetails = contextRouteService.getContextData(route);\n if (contextDetails.context !== ViewContext.Device) {\n return false;\n }\n\n const device = contextDetails.contextData as IManagedObject;\n if (!device || !Array.isArray(device.c8y_SupportedOperations)) {\n return false;\n }\n const supportedOperations: string[] = device.c8y_SupportedOperations;\n if (!supportedOperations.includes('c8y_RemoteAccessConnect')) {\n return false;\n }\n return remoteAccessService.isAvailable$();\n};\n\n@Injectable({\n providedIn: 'root'\n})\nexport class RemoteAccessService {\n private cachedIsAvailable$: Observable<boolean>;\n readonly baseUrl = '/service/remoteaccess';\n private document = inject(DOCUMENT);\n constructor(\n private fetchClient: FetchClient,\n private serviceRegistry: ServiceRegistry\n ) {}\n\n /**\n * Verifies if the remote access service is available by sending a HEAD request to is's health endpoint.\n * @returns cached Observable that emits true if the service is available, false otherwise.\n */\n isAvailable$(): Observable<boolean> {\n if (!this.cachedIsAvailable$) {\n this.cachedIsAvailable$ = defer(() => this.healthEndpointAvailable()).pipe(shareReplay(1));\n }\n\n return this.cachedIsAvailable$;\n }\n\n /**\n * misses the leading ? for the query params\n */\n getAuthQueryParamsForWebsocketConnection() {\n const { headers } = this.fetchClient.getFetchOptions();\n const params = new URLSearchParams();\n\n if (headers) {\n const xsrfToken = headers['X-XSRF-TOKEN'];\n const auth = headers['Authorization'];\n\n if (xsrfToken) {\n params.append('XSRF-TOKEN', xsrfToken);\n }\n if (auth) {\n params.append('token', auth.replace('Bearer ', '').replace('Basic ', ''));\n }\n }\n\n const paramsString = params.toString();\n return paramsString;\n }\n\n /**\n * Returns the URI for the websocket connection to the remote access service.\n */\n getWebSocketUri<K extends string, I extends string>(deviceId: K, configurationId: I) {\n const authQueryParams = this.getAuthQueryParamsForWebsocketConnection();\n const protocol = this.document.location.protocol === 'http:' ? 'ws' : 'wss';\n const pathName =\n `${protocol}://${this.document.location.host}${this.baseUrl}/client/${deviceId}/configurations/${configurationId}` as const;\n return authQueryParams ? (`${pathName}?${authQueryParams}` as const) : pathName;\n }\n\n /**\n * Retrieves all configurations for a given device.\n */\n async listConfigurations(deviceId: string): Promise<RemoteAccessConfiguration[]> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations`\n );\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to fetch configurations for device ${deviceId}`);\n }\n\n /**\n * Deletes a configuration for a given device.\n */\n async deleteConfiguration(deviceId: string, configurationId: string) {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations/${configurationId}`,\n { method: 'DELETE' }\n );\n\n if (response.ok) {\n return;\n }\n\n throw new Error(`Failed to delete configuration for device ${deviceId}`);\n }\n\n /**\n * Retrieves all available remote access protocol providers.\n */\n getProtocolProviders() {\n return this.serviceRegistry.get('remoteAccessProtocolHook');\n }\n\n /**\n * Retrieves all supported protocol providers for a given device.\n * Based on the declarations in the fragment c8y_RemoteAccessSupportedProtocols of the device managed object.\n */\n getSupportedProtocolProvidersFor(device: IManagedObject): RemoteAccessProtocolProvider[] {\n const { c8y_RemoteAccessSupportedProtocols = [] } = device;\n const uniqueInput = uniqWith(\n c8y_RemoteAccessSupportedProtocols.map(p => p.toUpperCase()),\n isEqual\n );\n const supportedProviders = intersectionWith(\n this.getProtocolProviders(),\n uniqueInput,\n ({ protocolName }, protocol) => protocolName?.toUpperCase() === protocol\n );\n return supportedProviders?.length > 0 ? supportedProviders : this.getProtocolProviders();\n }\n\n /**\n * Creates a new configuration for a given device.\n */\n async addConfiguration(\n deviceId: string,\n configuration: Omit<RemoteAccessConfiguration, 'id'>\n ): Promise<RemoteAccessConfiguration> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(configuration)\n }\n );\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to add configuration for device ${configuration.attrs.deviceId}`);\n }\n\n /**\n * Updates a configuration for a given device.\n */\n async updateConfiguration(\n deviceId: string,\n configuration: RemoteAccessConfiguration\n ): Promise<RemoteAccessConfiguration> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations/${configuration.id}`,\n {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(configuration)\n }\n );\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to update configuration for device ${configuration.attrs.deviceId}`);\n }\n\n /**\n * Persists a confirmed server host key onto a configuration (DM-6421 first-use flow). The stored\n * value is an OpenSSH authorized_keys-style string (`<keyType> <base64Blob>`).\n *\n * Uses the dedicated host-key endpoint so only the host key is sent — the untouched credential\n * secrets (password, private key, certificate) are never round-tripped through the client.\n */\n async updateHostKey(\n deviceId: string,\n configurationId: string,\n hostKey: string\n ): Promise<RemoteAccessConfiguration> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations/${configurationId}/hostkey`,\n {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ hostKey })\n }\n );\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to update host key for configuration ${configurationId}`);\n }\n\n /**\n * Whether the backend auto-trusts the server host key on first connect for the current tenant. When\n * enabled, the backend silently stores the key instead of prompting for confirmation.\n *\n * Delegates to the remote-access service rather than reading the `hostkey-autosave` tenant option\n * directly: the effective value is resolved server-side from the tenant option, owner inheritance\n * (a subtenant with no option set inherits the owner's) and the manifest default — none of which the\n * tenant-options API exposes to the client. Defaults to `false` (secure by default) when the status\n * cannot be read.\n */\n async isHostKeyAutosaveEnabled(): Promise<boolean> {\n try {\n const response = await this.fetchClient.fetch(`${this.baseUrl}/settings/hostkey-autosave`);\n if (!response.ok) {\n return false;\n }\n const status = await response.json();\n return status?.enabled === true;\n } catch {\n return false;\n }\n }\n\n /**\n * Generates a SSH key pair for a given hostname.\n */\n async generateKeyPair(hostname: string): Promise<{ publicKey: string; privateKey: string }> {\n const response = await this.fetchClient.fetch(`${this.baseUrl}/keypair/generate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ hostname })\n });\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to generate key pair for ${hostname}`);\n }\n\n private async healthEndpointAvailable(): Promise<boolean> {\n try {\n const response = await this.fetchClient.fetch(`${this.baseUrl}/health`, {\n method: 'HEAD',\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n\n if (response.ok) {\n return !!response.ok;\n }\n } catch {\n return false;\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"c8y-ngx-components-remote-access-data.mjs","sources":["../../remote-access/data/remote-access.service.ts","../../remote-access/data/c8y-ngx-components-remote-access-data.ts"],"sourcesContent":["import { DOCUMENT, inject, Injectable } from '@angular/core';\nimport { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router';\nimport { FetchClient, IManagedObject } from '@c8y/client';\nimport {\n ContextRouteService,\n Permissions,\n ServiceRegistry,\n ViewContext\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { Observable, defer, shareReplay } from 'rxjs';\nimport { RemoteAccessProtocolProvider } from './remote-access-protocol-provider';\nimport { uniqWith, intersectionWith, isEqual } from 'lodash';\n\nexport interface RemoteAccessConfiguration {\n id: string;\n name: string;\n hostname: string;\n port: number;\n protocol: string;\n attrs?: any;\n credentials?: any;\n credentialsType?: string;\n}\n\nexport const CREDENTIALS_TYPES = {\n NONE: {\n name: 'NONE',\n value: 'NONE',\n label: gettext('No password')\n },\n USER_PASS: {\n name: 'USER_PASS',\n value: 'USER_PASS',\n label: gettext('Username and password')\n },\n PASS_ONLY: {\n name: 'PASS_ONLY',\n value: 'PASS_ONLY',\n label: gettext('Password only')\n },\n KEY_PAIR: {\n name: 'KEY_PAIR',\n value: 'KEY_PAIR',\n label: gettext('Public/private keys')\n },\n CERTIFICATE: {\n name: 'CERTIFICATE',\n value: 'CERTIFICATE',\n label: gettext('Certificate')\n }\n} as const;\n\nexport const canActivateRemoteAccess: CanActivateFn = (route: ActivatedRouteSnapshot) => {\n const permissions = inject(Permissions);\n const remoteAccessService = inject(RemoteAccessService);\n const contextRouteService = inject(ContextRouteService);\n if (!permissions.hasRole(Permissions.ROLE_REMOTE_ACCESS_ADMIN)) {\n return false;\n }\n\n const contextDetails = contextRouteService.getContextData(route);\n if (contextDetails.context !== ViewContext.Device) {\n return false;\n }\n\n const device = contextDetails.contextData as IManagedObject;\n if (!device || !Array.isArray(device.c8y_SupportedOperations)) {\n return false;\n }\n const supportedOperations: string[] = device.c8y_SupportedOperations;\n if (!supportedOperations.includes('c8y_RemoteAccessConnect')) {\n return false;\n }\n return remoteAccessService.isAvailable$();\n};\n\n@Injectable({\n providedIn: 'root'\n})\nexport class RemoteAccessService {\n private cachedIsAvailable$: Observable<boolean>;\n readonly baseUrl = '/service/remoteaccess';\n private document = inject(DOCUMENT);\n constructor(\n private fetchClient: FetchClient,\n private serviceRegistry: ServiceRegistry\n ) {}\n\n /**\n * Verifies if the remote access service is available by sending a HEAD request to is's health endpoint.\n * @returns cached Observable that emits true if the service is available, false otherwise.\n */\n isAvailable$(): Observable<boolean> {\n if (!this.cachedIsAvailable$) {\n this.cachedIsAvailable$ = defer(() => this.healthEndpointAvailable()).pipe(shareReplay(1));\n }\n\n return this.cachedIsAvailable$;\n }\n\n /**\n * misses the leading ? for the query params\n */\n getAuthQueryParamsForWebsocketConnection() {\n const { headers } = this.fetchClient.getFetchOptions();\n const params = new URLSearchParams();\n\n if (headers) {\n const xsrfToken = headers['X-XSRF-TOKEN'];\n const auth = headers['Authorization'];\n\n if (xsrfToken) {\n params.append('XSRF-TOKEN', xsrfToken);\n }\n if (auth) {\n params.append('token', auth.replace('Bearer ', '').replace('Basic ', ''));\n }\n }\n\n const paramsString = params.toString();\n return paramsString;\n }\n\n /**\n * Returns the URI for the websocket connection to the remote access service.\n *\n * @param options.supportsHostKeyProbe when true, adds a `supportsHostKeyProbe=true` query parameter\n * telling the backend this UI can render the first-use SSH host-key probe frame (DM-6421). A browser\n * WebSocket cannot send custom headers, so the capability is negotiated via the URL. Older UIs omit\n * it, so its absence keeps the backend on the backward-compatible path (no probe).\n */\n getWebSocketUri(\n deviceId: string,\n configurationId: string,\n options: { supportsHostKeyProbe?: boolean } = {}\n ): string {\n const authQueryParams = this.getAuthQueryParamsForWebsocketConnection();\n const featureParams = options.supportsHostKeyProbe ? 'supportsHostKeyProbe=true' : '';\n const queryParams = [authQueryParams, featureParams].filter(Boolean).join('&');\n const protocol = this.document.location.protocol === 'http:' ? 'ws' : 'wss';\n const pathName = `${protocol}://${this.document.location.host}${this.baseUrl}/client/${deviceId}/configurations/${configurationId}`;\n return queryParams ? `${pathName}?${queryParams}` : pathName;\n }\n\n /**\n * Retrieves all configurations for a given device.\n */\n async listConfigurations(deviceId: string): Promise<RemoteAccessConfiguration[]> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations`\n );\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to fetch configurations for device ${deviceId}`);\n }\n\n /**\n * Deletes a configuration for a given device.\n */\n async deleteConfiguration(deviceId: string, configurationId: string) {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations/${configurationId}`,\n { method: 'DELETE' }\n );\n\n if (response.ok) {\n return;\n }\n\n throw new Error(`Failed to delete configuration for device ${deviceId}`);\n }\n\n /**\n * Retrieves all available remote access protocol providers.\n */\n getProtocolProviders() {\n return this.serviceRegistry.get('remoteAccessProtocolHook');\n }\n\n /**\n * Retrieves all supported protocol providers for a given device.\n * Based on the declarations in the fragment c8y_RemoteAccessSupportedProtocols of the device managed object.\n */\n getSupportedProtocolProvidersFor(device: IManagedObject): RemoteAccessProtocolProvider[] {\n const { c8y_RemoteAccessSupportedProtocols = [] } = device;\n const uniqueInput = uniqWith(\n c8y_RemoteAccessSupportedProtocols.map(p => p.toUpperCase()),\n isEqual\n );\n const supportedProviders = intersectionWith(\n this.getProtocolProviders(),\n uniqueInput,\n ({ protocolName }, protocol) => protocolName?.toUpperCase() === protocol\n );\n return supportedProviders?.length > 0 ? supportedProviders : this.getProtocolProviders();\n }\n\n /**\n * Creates a new configuration for a given device.\n */\n async addConfiguration(\n deviceId: string,\n configuration: Omit<RemoteAccessConfiguration, 'id'>\n ): Promise<RemoteAccessConfiguration> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(configuration)\n }\n );\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to add configuration for device ${configuration.attrs.deviceId}`);\n }\n\n /**\n * Updates a configuration for a given device.\n */\n async updateConfiguration(\n deviceId: string,\n configuration: RemoteAccessConfiguration\n ): Promise<RemoteAccessConfiguration> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations/${configuration.id}`,\n {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(configuration)\n }\n );\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to update configuration for device ${configuration.attrs.deviceId}`);\n }\n\n /**\n * Persists a confirmed server host key onto a configuration (DM-6421 first-use flow). The stored\n * value is an OpenSSH authorized_keys-style string (`<keyType> <base64Blob>`).\n *\n * Uses the dedicated host-key endpoint so only the host key is sent — the untouched credential\n * secrets (password, private key, certificate) are never round-tripped through the client.\n */\n async updateHostKey(\n deviceId: string,\n configurationId: string,\n hostKey: string\n ): Promise<RemoteAccessConfiguration> {\n const response = await this.fetchClient.fetch(\n `${this.baseUrl}/devices/${deviceId}/configurations/${configurationId}/hostkey`,\n {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ hostKey })\n }\n );\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to update host key for configuration ${configurationId}`);\n }\n\n /**\n * Whether the backend auto-trusts the server host key on first connect for the current tenant. When\n * enabled, the backend silently stores the key instead of prompting for confirmation.\n *\n * Delegates to the remote-access service rather than reading the `hostkey-autosave` tenant option\n * directly: the effective value is resolved server-side from the tenant option, owner inheritance\n * (a subtenant with no option set inherits the owner's) and the manifest default — none of which the\n * tenant-options API exposes to the client. Defaults to `false` (secure by default) when the status\n * cannot be read.\n */\n async isHostKeyAutosaveEnabled(): Promise<boolean> {\n try {\n const response = await this.fetchClient.fetch(`${this.baseUrl}/settings/hostkey-autosave`);\n if (!response.ok) {\n return false;\n }\n const status = await response.json();\n return status?.enabled === true;\n } catch {\n return false;\n }\n }\n\n /**\n * Generates a SSH key pair for a given hostname.\n */\n async generateKeyPair(hostname: string): Promise<{ publicKey: string; privateKey: string }> {\n const response = await this.fetchClient.fetch(`${this.baseUrl}/keypair/generate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ hostname })\n });\n\n if (response.ok) {\n return response.json();\n }\n\n throw new Error(`Failed to generate key pair for ${hostname}`);\n }\n\n private async healthEndpointAvailable(): Promise<boolean> {\n try {\n const response = await this.fetchClient.fetch(`${this.baseUrl}/health`, {\n method: 'HEAD',\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n\n if (response.ok) {\n return !!response.ok;\n }\n } catch {\n return false;\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -247,7 +247,12 @@ class TerminalViewerComponent {
|
|
|
247
247
|
this.closeSocket();
|
|
248
248
|
this.firstDeviceMessageReceived.set(false);
|
|
249
249
|
this.awaitingHostKeyConfirmation = false;
|
|
250
|
-
this.socket = new WebSocket(
|
|
250
|
+
this.socket = new WebSocket(
|
|
251
|
+
// This terminal renders the first-use SSH host-key probe frame, so advertise that capability
|
|
252
|
+
// to the backend (DM-6421); without it the backend skips the probe.
|
|
253
|
+
this.remoteAccess.getWebSocketUri(this.deviceId, this.configurationId, {
|
|
254
|
+
supportsHostKeyProbe: true
|
|
255
|
+
}), 'binary');
|
|
251
256
|
this.socket.binaryType = 'arraybuffer';
|
|
252
257
|
this.terminal.writeln('[92mEstablishing Websocket connection... [39m \r\n');
|
|
253
258
|
this.socket.onopen = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"c8y-ngx-components-remote-access-terminal-viewer.mjs","sources":["../../remote-access/terminal-viewer/telnet-negotiator.ts","../../remote-access/terminal-viewer/shell-adapter.ts","../../remote-access/terminal-viewer/host-key.model.ts","../../remote-access/terminal-viewer/terminal-viewer.component.ts","../../remote-access/terminal-viewer/terminal-viewer.component.html","../../remote-access/terminal-viewer/c8y-ngx-components-remote-access-terminal-viewer.ts"],"sourcesContent":["import type { Terminal } from '@xterm/xterm';\n\nexport class TelnetNegotiator {\n constructor(\n private terminal: Terminal,\n private termType: string\n ) {}\n\n setTermType(type) {\n this.termType = type;\n }\n\n pushStr(str, arr) {\n for (let i = 0; i < str.length; i++) {\n arr.push(str.charCodeAt(i));\n }\n }\n\n negotiate(data) {\n const arrUint8 = new Uint8Array(data);\n const arr = [...arrUint8.values()];\n\n const sendQueue = [];\n let stringToDisplay = '';\n let chr: number;\n let code: number;\n let value: number;\n\n while (arr.length > 0) {\n chr = arr.shift();\n switch (chr) {\n case 255: // IAC\n code = arr.shift();\n value = arr.shift();\n switch (code) {\n case 253: // DO\n if (value === 24) {\n // Terminal type\n sendQueue.push(255, 251, value);\n } else if (value === 31) {\n // NAWS (Negotiate About Window Size)\n // client answers WILL NAWS\n sendQueue.push(255, 251, 31);\n // client sends IAC SB NAWS 0 terminalSize.cols 0 terminalSize.rows IAC SE\n sendQueue.push(\n 255,\n 250,\n 31,\n 0,\n this.terminal.cols,\n 0,\n this.terminal.rows,\n 255,\n 240\n );\n } else {\n // Refuse other DO requests with a WONT\n sendQueue.push(255, 252, value);\n }\n break;\n case 251: // WILL\n if (value === 1) {\n // Affirm echo with DO\n sendQueue.push(255, 253, value);\n } else {\n // Reject other WILL offers with a DONT\n sendQueue.push(255, 254, value);\n }\n break;\n case 250: // SB (subnegotiation)\n if (value === 24) {\n // TERM-TYPE subnegotiation\n if (arr[0] === 1 && arr[1] === 255 && arr[2] === 240) {\n arr.shift();\n arr.shift();\n arr.shift();\n sendQueue.push(255, 250, 24, 0);\n this.pushStr(this.termType, sendQueue);\n sendQueue.push(255, 240);\n } else {\n console.warn('Invalid subnegotiation received' + arr);\n }\n } else {\n console.warn('Ignoring SB ' + value);\n }\n break;\n case 254: // DONT\n case 252: // WONT\n default:\n break;\n }\n break;\n case 242: // Data Mark (Synch)\n code = arr.shift();\n value = arr.shift();\n break;\n default: // everything else\n stringToDisplay += String.fromCharCode(chr);\n break;\n }\n }\n\n return {\n dataToSend: sendQueue.length ? new Uint8Array(sendQueue) : '',\n isOutput: !!stringToDisplay.length\n };\n }\n}\n","import { TelnetNegotiator } from './telnet-negotiator';\nimport type { Terminal } from '@xterm/xterm';\n\nexport class ShellAdapter {\n constructor(\n private terminal: Terminal,\n private termType = 'xterm-256color'\n ) {}\n\n filterNonPrintable(str: string) {\n // get rid of �\n const helpArr = str.split('�');\n str = helpArr.join('');\n\n return str;\n }\n\n setTermType(type) {\n this.termType = type;\n }\n\n filterReceiveData(data) {\n // negotiate telnet signals, if any\n const telnet = new TelnetNegotiator(this.terminal, this.termType);\n const telnetData = telnet.negotiate(data);\n\n // decode text for display\n const textDecoder = new TextDecoder();\n const decodedToDisplay = textDecoder.decode(data);\n\n return {\n dataToSend: telnetData.dataToSend,\n dataToDisplay: telnetData.isOutput ? this.filterNonPrintable(decodedToDisplay) : ''\n };\n }\n\n filterSendData(data) {\n const sendQueue = [];\n for (let i = 0; i < data.length; i++) {\n sendQueue.push(data.charCodeAt(i));\n }\n\n return sendQueue.length ? new Uint8Array(sendQueue) : '';\n }\n}\n","/**\n * Wire model for the Cloud Remote Access first-use host-key confirmation flow (DM-6421).\n *\n * The remote-access WebSocket normally carries a raw telnet/binary shell stream. When the backend\n * runs a host-key probe (first use, `hostkey-autosave=false`, no key stored yet) it prepends a single\n * JSON control frame — delivered as a binary WebSocket frame — and then tears the socket down. The UI\n * distinguishes such a control frame from terminal bytes via the `type` discriminator.\n */\n\n/** Server host key captured during KEX, pushed to the UI for confirmation. */\nexport interface HostKeyProbeMessage {\n type: 'hostkey';\n hostname: string;\n port: number;\n /** SSH key type, e.g. `ssh-ed25519`, `ssh-rsa`. */\n keyType: string;\n /** Base64 of the raw SSH wire encoding of the key. */\n publicKey: string;\n /** OpenSSH-style SHA-256 fingerprint, e.g. `SHA256:abc123…`. Shown to the user for confirmation. */\n fingerprintSha256: string;\n /**\n * `null` = nothing stored yet (genuine first use); `true` = live key equals the stored key (safe);\n * `false` = live key differs from the stored key (rotation or MITM).\n */\n matchesStored: boolean | null;\n}\n\n/** Error raised while probing; surfaced to the user instead of a terminal session. */\nexport interface HostKeyErrorMessage {\n type: 'error';\n /** Machine-readable error identifier (contract shared with the backend `sendProbeError` frame). */\n code?: string;\n message: string;\n}\n\nexport type RemoteAccessControlFrame = HostKeyProbeMessage | HostKeyErrorMessage;\n\n/**\n * Attempts to parse an incoming WebSocket frame as a remote-access control frame. Returns the parsed\n * frame for a recognised `type`, or `null` when the data is a raw terminal byte stream (the normal\n * case) — the caller then falls through to the telnet/xterm path. Never throws.\n */\nexport function parseControlFrame(data: unknown): RemoteAccessControlFrame | null {\n if (data === null || typeof data !== 'object') {\n return null;\n }\n let bytes: Uint8Array;\n try {\n // The socket uses binaryType='arraybuffer', so frames arrive as ArrayBuffer. Constructing a\n // Uint8Array works across JS realms, unlike `instanceof ArrayBuffer` (which breaks under jsdom).\n bytes = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer);\n } catch {\n return null;\n }\n // Control frames are JSON objects; a raw shell stream almost never starts with '{' (0x7b), so this\n // cheaply skips the JSON.parse attempt for terminal data.\n if (bytes.length === 0 || bytes[0] !== 0x7b) {\n return null;\n }\n try {\n const parsed = JSON.parse(new TextDecoder().decode(bytes));\n if (parsed?.type === 'hostkey' || parsed?.type === 'error') {\n return parsed as RemoteAccessControlFrame;\n }\n } catch {\n // Not JSON — raw terminal data that happened to start with '{'. Fall through to the terminal path.\n }\n return null;\n}\n\n/** Composes the OpenSSH authorized_keys-style string persisted in the configuration credentials. */\nexport function toStoredHostKey(frame: HostKeyProbeMessage): string {\n return `${frame.keyType} ${frame.publicKey}`;\n}\n","import {\n AfterViewInit,\n ChangeDetectionStrategy,\n Component,\n OnDestroy,\n signal,\n ViewEncapsulation\n} from '@angular/core';\nimport {\n ActionBarItemComponent,\n AlertService,\n C8yTranslatePipe,\n IconDirective,\n ModalService,\n Status,\n TitleComponent\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TranslateService } from '@ngx-translate/core';\nimport { Terminal } from '@xterm/xterm';\nimport { FitAddon } from '@xterm/addon-fit';\nimport { ShellAdapter } from './shell-adapter';\nimport { RemoteAccessService } from '@c8y/ngx-components/remote-access/data';\nimport { ActivatedRoute } from '@angular/router';\nimport {\n HostKeyProbeMessage,\n parseControlFrame,\n RemoteAccessControlFrame,\n toStoredHostKey\n} from './host-key.model';\n\n@Component({\n selector: 'c8y-terminal-viewer',\n templateUrl: './terminal-viewer.component.html',\n styleUrls: ['./terminal-viewer.component.scss'],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [TitleComponent, C8yTranslatePipe, ActionBarItemComponent, IconDirective]\n})\nexport class TerminalViewerComponent implements AfterViewInit, OnDestroy {\n title = '';\n container: HTMLElement | null = null;\n terminal: Terminal | null = null;\n socket: WebSocket | null = null;\n /** True once real terminal bytes start flowing (not while a host-key control frame is handled). */\n readonly firstDeviceMessageReceived = signal(false);\n readonly configurationId: string;\n readonly deviceId: string;\n protected observer: ResizeObserver;\n\n private shellAdapter: ShellAdapter;\n /** Suppresses the \"device disconnected\" banner while a probe socket is torn down for confirmation. */\n private awaitingHostKeyConfirmation = false;\n\n constructor(\n private remoteAccess: RemoteAccessService,\n private activatedRoute: ActivatedRoute,\n private modalService: ModalService,\n private alertService: AlertService,\n private translateService: TranslateService\n ) {\n this.configurationId = this.activatedRoute.snapshot.params.configurationId;\n this.deviceId = this.activatedRoute.parent.snapshot.params.id;\n }\n\n ngOnDestroy(): void {\n this.observer?.disconnect();\n this.closeSocket();\n }\n\n ngAfterViewInit(): void {\n this.container = document.getElementById('terminal-screen');\n this.terminal = new Terminal({\n fontSize: 18,\n fontFamily: 'consolas, monospace',\n cursorBlink: true\n });\n const fitAddon = new FitAddon();\n this.terminal.loadAddon(fitAddon);\n this.terminal.open(this.container);\n fitAddon.fit();\n this.shellAdapter = new ShellAdapter(this.terminal);\n this.observer = new ResizeObserver(() => fitAddon.fit());\n this.observer.observe(this.container);\n this.terminal.focus();\n\n this.terminal.onData(data => {\n if (this.firstDeviceMessageReceived()) {\n const encodedToSend = this.shellAdapter.filterSendData(data);\n if (encodedToSend.length) {\n this.socket?.send(encodedToSend);\n }\n }\n });\n\n this.connect();\n }\n\n toggleFullscreen() {\n if (document.fullscreenElement) {\n document.exitFullscreen();\n } else {\n this.container.requestFullscreen();\n }\n }\n\n /** Opens (or re-opens) the remote-access WebSocket and wires up the frame handlers. */\n private connect(): void {\n this.closeSocket();\n this.firstDeviceMessageReceived.set(false);\n this.awaitingHostKeyConfirmation = false;\n\n this.socket = new WebSocket(\n this.remoteAccess.getWebSocketUri(this.deviceId, this.configurationId),\n 'binary'\n );\n this.socket.binaryType = 'arraybuffer';\n\n this.terminal.writeln('\u001b[92mEstablishing Websocket connection... \u001b[39m \\r\\n');\n this.socket.onopen = () => {\n this.terminal.writeln(\n '\u001b[92mWebsocket connection was established successfully, waiting for device... \u001b[39m \\r\\n'\n );\n };\n\n this.socket.onclose = () => {\n if (this.awaitingHostKeyConfirmation) {\n return;\n }\n this.terminal.writeln('');\n this.terminal.writeln('\\r\\n\u001b[91mDevice disconnected. \u001b[39m \\r\\n');\n this.terminal.writeln('\\r\\n\u001b[91mWebsocket connection was closed. \u001b[39m \\r\\n');\n };\n\n this.socket.onmessage = message => {\n // On first use the backend prepends a single JSON control frame (host-key probe or error) and\n // then closes the socket. Everything else is a raw telnet/binary shell stream handled as before.\n if (!this.firstDeviceMessageReceived()) {\n const frame = parseControlFrame(message.data);\n if (frame) {\n this.handleControlFrame(frame);\n return;\n }\n this.firstDeviceMessageReceived.set(true);\n this.terminal.writeln('\u001b[92mDevice connection was established successfully. \u001b[39m \\r\\n');\n }\n const filteredData = this.shellAdapter.filterReceiveData(message.data);\n if (filteredData.dataToSend.length) {\n this.socket.send(filteredData.dataToSend);\n }\n if (filteredData.dataToDisplay.length) {\n this.terminal.write(filteredData.dataToDisplay);\n }\n };\n }\n\n private handleControlFrame(frame: RemoteAccessControlFrame): void {\n if (frame.type === 'hostkey') {\n this.awaitingHostKeyConfirmation = true;\n void this.confirmHostKey(frame);\n } else {\n this.alertService.danger(\n this.translateService.instant(gettext('Host key verification failed.')),\n frame.message\n );\n }\n }\n\n /** Shows the fingerprint confirmation dialog; on accept persists the key and reconnects. */\n private async confirmHostKey(frame: HostKeyProbeMessage): Promise<void> {\n const changed = frame.matchesStored === false;\n try {\n await this.modalService.confirm(\n this.translateService.instant(\n changed ? gettext('Host key changed') : gettext('Confirm host key')\n ),\n this.buildConfirmBody(frame, changed),\n changed ? Status.DANGER : Status.INFO,\n { ok: this.translateService.instant(gettext('Trust and connect')) },\n {},\n undefined,\n // Require the user to retype a code before trusting a key that differs from the stored one.\n changed\n );\n } catch {\n // Dismissed: abort without persisting the key and without opening a session.\n this.terminal.writeln(\n `\\r\\n\u001b[91m${this.translateService.instant(\n gettext('Host key was not confirmed. Connection aborted.')\n )} \u001b[39m \\r\\n`\n );\n return;\n }\n\n try {\n await this.remoteAccess.updateHostKey(\n this.deviceId,\n this.configurationId,\n toStoredHostKey(frame)\n );\n this.connect();\n } catch {\n this.alertService.danger(\n this.translateService.instant(gettext('Failed to save the host key.'))\n );\n }\n }\n\n private buildConfirmBody(frame: HostKeyProbeMessage, changed: boolean): string {\n const endpoint = `<code>${this.escapeHtml(frame.hostname)}:${Number(frame.port)}</code>`;\n const intro = changed\n ? this.translateService.instant(\n gettext(\n 'The host key for {{ endpoint }} has changed since it was last stored. This can happen after a legitimate key rotation, but it can also indicate a man-in-the-middle attack. Only continue if you trust the new key.'\n ),\n { endpoint }\n )\n : this.translateService.instant(\n gettext(\n 'Confirm the host key for {{ endpoint }}. Only continue if the fingerprint matches the server you expect.'\n ),\n { endpoint }\n );\n const keyType = this.translateService.instant(gettext('Key type: {{ keyType }}'), {\n keyType: this.escapeHtml(frame.keyType)\n });\n const fingerprint = this.translateService.instant(\n gettext('Fingerprint (SHA-256): {{ value }}'),\n {\n value: `<code>${this.escapeHtml(frame.fingerprintSha256)}</code>`\n }\n );\n return `${intro}<br><br>${keyType}<br>${fingerprint}`;\n }\n\n /** Escapes values interpolated into the modal's HTML body to prevent markup injection. */\n private escapeHtml(value: string): string {\n const entities: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(value).replace(/[&<>\"']/g, char => entities[char]);\n }\n\n private closeSocket(): void {\n if (!this.socket) {\n return;\n }\n const socket = this.socket;\n this.socket = null;\n // Detach the close handler before closing so our own teardown never triggers the\n // \"device disconnected\" banner — the async onclose would otherwise fire after this returns.\n socket.onclose = null;\n const stringToSend = 'exit\\n';\n const sendQueue = [];\n for (let i = 0; i < stringToSend.length; i++) {\n sendQueue.push(stringToSend.charCodeAt(i));\n }\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(new Uint8Array(sendQueue));\n }\n socket.close();\n }\n}\n","<c8y-title>Terminal Viewer: {{ title | translate }}</c8y-title>\n\n@if (firstDeviceMessageReceived()) {\n <c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n (click)=\"toggleFullscreen()\"\n >\n <i [c8yIcon]=\"'expand'\"></i>\n <span translate>Fullscreen</span>\n </button>\n </c8y-action-bar-item>\n}\n\n<div class=\"content-fullpage\">\n <div\n class=\"inner-scroll\"\n id=\"terminal-screen\"\n ></div>\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"c8y-ngx-components-remote-access-terminal-viewer.mjs","sources":["../../remote-access/terminal-viewer/telnet-negotiator.ts","../../remote-access/terminal-viewer/shell-adapter.ts","../../remote-access/terminal-viewer/host-key.model.ts","../../remote-access/terminal-viewer/terminal-viewer.component.ts","../../remote-access/terminal-viewer/terminal-viewer.component.html","../../remote-access/terminal-viewer/c8y-ngx-components-remote-access-terminal-viewer.ts"],"sourcesContent":["import type { Terminal } from '@xterm/xterm';\n\nexport class TelnetNegotiator {\n constructor(\n private terminal: Terminal,\n private termType: string\n ) {}\n\n setTermType(type) {\n this.termType = type;\n }\n\n pushStr(str, arr) {\n for (let i = 0; i < str.length; i++) {\n arr.push(str.charCodeAt(i));\n }\n }\n\n negotiate(data) {\n const arrUint8 = new Uint8Array(data);\n const arr = [...arrUint8.values()];\n\n const sendQueue = [];\n let stringToDisplay = '';\n let chr: number;\n let code: number;\n let value: number;\n\n while (arr.length > 0) {\n chr = arr.shift();\n switch (chr) {\n case 255: // IAC\n code = arr.shift();\n value = arr.shift();\n switch (code) {\n case 253: // DO\n if (value === 24) {\n // Terminal type\n sendQueue.push(255, 251, value);\n } else if (value === 31) {\n // NAWS (Negotiate About Window Size)\n // client answers WILL NAWS\n sendQueue.push(255, 251, 31);\n // client sends IAC SB NAWS 0 terminalSize.cols 0 terminalSize.rows IAC SE\n sendQueue.push(\n 255,\n 250,\n 31,\n 0,\n this.terminal.cols,\n 0,\n this.terminal.rows,\n 255,\n 240\n );\n } else {\n // Refuse other DO requests with a WONT\n sendQueue.push(255, 252, value);\n }\n break;\n case 251: // WILL\n if (value === 1) {\n // Affirm echo with DO\n sendQueue.push(255, 253, value);\n } else {\n // Reject other WILL offers with a DONT\n sendQueue.push(255, 254, value);\n }\n break;\n case 250: // SB (subnegotiation)\n if (value === 24) {\n // TERM-TYPE subnegotiation\n if (arr[0] === 1 && arr[1] === 255 && arr[2] === 240) {\n arr.shift();\n arr.shift();\n arr.shift();\n sendQueue.push(255, 250, 24, 0);\n this.pushStr(this.termType, sendQueue);\n sendQueue.push(255, 240);\n } else {\n console.warn('Invalid subnegotiation received' + arr);\n }\n } else {\n console.warn('Ignoring SB ' + value);\n }\n break;\n case 254: // DONT\n case 252: // WONT\n default:\n break;\n }\n break;\n case 242: // Data Mark (Synch)\n code = arr.shift();\n value = arr.shift();\n break;\n default: // everything else\n stringToDisplay += String.fromCharCode(chr);\n break;\n }\n }\n\n return {\n dataToSend: sendQueue.length ? new Uint8Array(sendQueue) : '',\n isOutput: !!stringToDisplay.length\n };\n }\n}\n","import { TelnetNegotiator } from './telnet-negotiator';\nimport type { Terminal } from '@xterm/xterm';\n\nexport class ShellAdapter {\n constructor(\n private terminal: Terminal,\n private termType = 'xterm-256color'\n ) {}\n\n filterNonPrintable(str: string) {\n // get rid of �\n const helpArr = str.split('�');\n str = helpArr.join('');\n\n return str;\n }\n\n setTermType(type) {\n this.termType = type;\n }\n\n filterReceiveData(data) {\n // negotiate telnet signals, if any\n const telnet = new TelnetNegotiator(this.terminal, this.termType);\n const telnetData = telnet.negotiate(data);\n\n // decode text for display\n const textDecoder = new TextDecoder();\n const decodedToDisplay = textDecoder.decode(data);\n\n return {\n dataToSend: telnetData.dataToSend,\n dataToDisplay: telnetData.isOutput ? this.filterNonPrintable(decodedToDisplay) : ''\n };\n }\n\n filterSendData(data) {\n const sendQueue = [];\n for (let i = 0; i < data.length; i++) {\n sendQueue.push(data.charCodeAt(i));\n }\n\n return sendQueue.length ? new Uint8Array(sendQueue) : '';\n }\n}\n","/**\n * Wire model for the Cloud Remote Access first-use host-key confirmation flow (DM-6421).\n *\n * The remote-access WebSocket normally carries a raw telnet/binary shell stream. When the backend\n * runs a host-key probe (first use, `hostkey-autosave=false`, no key stored yet) it prepends a single\n * JSON control frame — delivered as a binary WebSocket frame — and then tears the socket down. The UI\n * distinguishes such a control frame from terminal bytes via the `type` discriminator.\n */\n\n/** Server host key captured during KEX, pushed to the UI for confirmation. */\nexport interface HostKeyProbeMessage {\n type: 'hostkey';\n hostname: string;\n port: number;\n /** SSH key type, e.g. `ssh-ed25519`, `ssh-rsa`. */\n keyType: string;\n /** Base64 of the raw SSH wire encoding of the key. */\n publicKey: string;\n /** OpenSSH-style SHA-256 fingerprint, e.g. `SHA256:abc123…`. Shown to the user for confirmation. */\n fingerprintSha256: string;\n /**\n * `null` = nothing stored yet (genuine first use); `true` = live key equals the stored key (safe);\n * `false` = live key differs from the stored key (rotation or MITM).\n */\n matchesStored: boolean | null;\n}\n\n/** Error raised while probing; surfaced to the user instead of a terminal session. */\nexport interface HostKeyErrorMessage {\n type: 'error';\n /** Machine-readable error identifier (contract shared with the backend `sendProbeError` frame). */\n code?: string;\n message: string;\n}\n\nexport type RemoteAccessControlFrame = HostKeyProbeMessage | HostKeyErrorMessage;\n\n/**\n * Attempts to parse an incoming WebSocket frame as a remote-access control frame. Returns the parsed\n * frame for a recognised `type`, or `null` when the data is a raw terminal byte stream (the normal\n * case) — the caller then falls through to the telnet/xterm path. Never throws.\n */\nexport function parseControlFrame(data: unknown): RemoteAccessControlFrame | null {\n if (data === null || typeof data !== 'object') {\n return null;\n }\n let bytes: Uint8Array;\n try {\n // The socket uses binaryType='arraybuffer', so frames arrive as ArrayBuffer. Constructing a\n // Uint8Array works across JS realms, unlike `instanceof ArrayBuffer` (which breaks under jsdom).\n bytes = data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer);\n } catch {\n return null;\n }\n // Control frames are JSON objects; a raw shell stream almost never starts with '{' (0x7b), so this\n // cheaply skips the JSON.parse attempt for terminal data.\n if (bytes.length === 0 || bytes[0] !== 0x7b) {\n return null;\n }\n try {\n const parsed = JSON.parse(new TextDecoder().decode(bytes));\n if (parsed?.type === 'hostkey' || parsed?.type === 'error') {\n return parsed as RemoteAccessControlFrame;\n }\n } catch {\n // Not JSON — raw terminal data that happened to start with '{'. Fall through to the terminal path.\n }\n return null;\n}\n\n/** Composes the OpenSSH authorized_keys-style string persisted in the configuration credentials. */\nexport function toStoredHostKey(frame: HostKeyProbeMessage): string {\n return `${frame.keyType} ${frame.publicKey}`;\n}\n","import {\n AfterViewInit,\n ChangeDetectionStrategy,\n Component,\n OnDestroy,\n signal,\n ViewEncapsulation\n} from '@angular/core';\nimport {\n ActionBarItemComponent,\n AlertService,\n C8yTranslatePipe,\n IconDirective,\n ModalService,\n Status,\n TitleComponent\n} from '@c8y/ngx-components';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport { TranslateService } from '@ngx-translate/core';\nimport { Terminal } from '@xterm/xterm';\nimport { FitAddon } from '@xterm/addon-fit';\nimport { ShellAdapter } from './shell-adapter';\nimport { RemoteAccessService } from '@c8y/ngx-components/remote-access/data';\nimport { ActivatedRoute } from '@angular/router';\nimport {\n HostKeyProbeMessage,\n parseControlFrame,\n RemoteAccessControlFrame,\n toStoredHostKey\n} from './host-key.model';\n\n@Component({\n selector: 'c8y-terminal-viewer',\n templateUrl: './terminal-viewer.component.html',\n styleUrls: ['./terminal-viewer.component.scss'],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [TitleComponent, C8yTranslatePipe, ActionBarItemComponent, IconDirective]\n})\nexport class TerminalViewerComponent implements AfterViewInit, OnDestroy {\n title = '';\n container: HTMLElement | null = null;\n terminal: Terminal | null = null;\n socket: WebSocket | null = null;\n /** True once real terminal bytes start flowing (not while a host-key control frame is handled). */\n readonly firstDeviceMessageReceived = signal(false);\n readonly configurationId: string;\n readonly deviceId: string;\n protected observer: ResizeObserver;\n\n private shellAdapter: ShellAdapter;\n /** Suppresses the \"device disconnected\" banner while a probe socket is torn down for confirmation. */\n private awaitingHostKeyConfirmation = false;\n\n constructor(\n private remoteAccess: RemoteAccessService,\n private activatedRoute: ActivatedRoute,\n private modalService: ModalService,\n private alertService: AlertService,\n private translateService: TranslateService\n ) {\n this.configurationId = this.activatedRoute.snapshot.params.configurationId;\n this.deviceId = this.activatedRoute.parent.snapshot.params.id;\n }\n\n ngOnDestroy(): void {\n this.observer?.disconnect();\n this.closeSocket();\n }\n\n ngAfterViewInit(): void {\n this.container = document.getElementById('terminal-screen');\n this.terminal = new Terminal({\n fontSize: 18,\n fontFamily: 'consolas, monospace',\n cursorBlink: true\n });\n const fitAddon = new FitAddon();\n this.terminal.loadAddon(fitAddon);\n this.terminal.open(this.container);\n fitAddon.fit();\n this.shellAdapter = new ShellAdapter(this.terminal);\n this.observer = new ResizeObserver(() => fitAddon.fit());\n this.observer.observe(this.container);\n this.terminal.focus();\n\n this.terminal.onData(data => {\n if (this.firstDeviceMessageReceived()) {\n const encodedToSend = this.shellAdapter.filterSendData(data);\n if (encodedToSend.length) {\n this.socket?.send(encodedToSend);\n }\n }\n });\n\n this.connect();\n }\n\n toggleFullscreen() {\n if (document.fullscreenElement) {\n document.exitFullscreen();\n } else {\n this.container.requestFullscreen();\n }\n }\n\n /** Opens (or re-opens) the remote-access WebSocket and wires up the frame handlers. */\n private connect(): void {\n this.closeSocket();\n this.firstDeviceMessageReceived.set(false);\n this.awaitingHostKeyConfirmation = false;\n\n this.socket = new WebSocket(\n // This terminal renders the first-use SSH host-key probe frame, so advertise that capability\n // to the backend (DM-6421); without it the backend skips the probe.\n this.remoteAccess.getWebSocketUri(this.deviceId, this.configurationId, {\n supportsHostKeyProbe: true\n }),\n 'binary'\n );\n this.socket.binaryType = 'arraybuffer';\n\n this.terminal.writeln('\u001b[92mEstablishing Websocket connection... \u001b[39m \\r\\n');\n this.socket.onopen = () => {\n this.terminal.writeln(\n '\u001b[92mWebsocket connection was established successfully, waiting for device... \u001b[39m \\r\\n'\n );\n };\n\n this.socket.onclose = () => {\n if (this.awaitingHostKeyConfirmation) {\n return;\n }\n this.terminal.writeln('');\n this.terminal.writeln('\\r\\n\u001b[91mDevice disconnected. \u001b[39m \\r\\n');\n this.terminal.writeln('\\r\\n\u001b[91mWebsocket connection was closed. \u001b[39m \\r\\n');\n };\n\n this.socket.onmessage = message => {\n // On first use the backend prepends a single JSON control frame (host-key probe or error) and\n // then closes the socket. Everything else is a raw telnet/binary shell stream handled as before.\n if (!this.firstDeviceMessageReceived()) {\n const frame = parseControlFrame(message.data);\n if (frame) {\n this.handleControlFrame(frame);\n return;\n }\n this.firstDeviceMessageReceived.set(true);\n this.terminal.writeln('\u001b[92mDevice connection was established successfully. \u001b[39m \\r\\n');\n }\n const filteredData = this.shellAdapter.filterReceiveData(message.data);\n if (filteredData.dataToSend.length) {\n this.socket.send(filteredData.dataToSend);\n }\n if (filteredData.dataToDisplay.length) {\n this.terminal.write(filteredData.dataToDisplay);\n }\n };\n }\n\n private handleControlFrame(frame: RemoteAccessControlFrame): void {\n if (frame.type === 'hostkey') {\n this.awaitingHostKeyConfirmation = true;\n void this.confirmHostKey(frame);\n } else {\n this.alertService.danger(\n this.translateService.instant(gettext('Host key verification failed.')),\n frame.message\n );\n }\n }\n\n /** Shows the fingerprint confirmation dialog; on accept persists the key and reconnects. */\n private async confirmHostKey(frame: HostKeyProbeMessage): Promise<void> {\n const changed = frame.matchesStored === false;\n try {\n await this.modalService.confirm(\n this.translateService.instant(\n changed ? gettext('Host key changed') : gettext('Confirm host key')\n ),\n this.buildConfirmBody(frame, changed),\n changed ? Status.DANGER : Status.INFO,\n { ok: this.translateService.instant(gettext('Trust and connect')) },\n {},\n undefined,\n // Require the user to retype a code before trusting a key that differs from the stored one.\n changed\n );\n } catch {\n // Dismissed: abort without persisting the key and without opening a session.\n this.terminal.writeln(\n `\\r\\n\u001b[91m${this.translateService.instant(\n gettext('Host key was not confirmed. Connection aborted.')\n )} \u001b[39m \\r\\n`\n );\n return;\n }\n\n try {\n await this.remoteAccess.updateHostKey(\n this.deviceId,\n this.configurationId,\n toStoredHostKey(frame)\n );\n this.connect();\n } catch {\n this.alertService.danger(\n this.translateService.instant(gettext('Failed to save the host key.'))\n );\n }\n }\n\n private buildConfirmBody(frame: HostKeyProbeMessage, changed: boolean): string {\n const endpoint = `<code>${this.escapeHtml(frame.hostname)}:${Number(frame.port)}</code>`;\n const intro = changed\n ? this.translateService.instant(\n gettext(\n 'The host key for {{ endpoint }} has changed since it was last stored. This can happen after a legitimate key rotation, but it can also indicate a man-in-the-middle attack. Only continue if you trust the new key.'\n ),\n { endpoint }\n )\n : this.translateService.instant(\n gettext(\n 'Confirm the host key for {{ endpoint }}. Only continue if the fingerprint matches the server you expect.'\n ),\n { endpoint }\n );\n const keyType = this.translateService.instant(gettext('Key type: {{ keyType }}'), {\n keyType: this.escapeHtml(frame.keyType)\n });\n const fingerprint = this.translateService.instant(\n gettext('Fingerprint (SHA-256): {{ value }}'),\n {\n value: `<code>${this.escapeHtml(frame.fingerprintSha256)}</code>`\n }\n );\n return `${intro}<br><br>${keyType}<br>${fingerprint}`;\n }\n\n /** Escapes values interpolated into the modal's HTML body to prevent markup injection. */\n private escapeHtml(value: string): string {\n const entities: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return String(value).replace(/[&<>\"']/g, char => entities[char]);\n }\n\n private closeSocket(): void {\n if (!this.socket) {\n return;\n }\n const socket = this.socket;\n this.socket = null;\n // Detach the close handler before closing so our own teardown never triggers the\n // \"device disconnected\" banner — the async onclose would otherwise fire after this returns.\n socket.onclose = null;\n const stringToSend = 'exit\\n';\n const sendQueue = [];\n for (let i = 0; i < stringToSend.length; i++) {\n sendQueue.push(stringToSend.charCodeAt(i));\n }\n if (socket.readyState === WebSocket.OPEN) {\n socket.send(new Uint8Array(sendQueue));\n }\n socket.close();\n }\n}\n","<c8y-title>Terminal Viewer: {{ title | translate }}</c8y-title>\n\n@if (firstDeviceMessageReceived()) {\n <c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n (click)=\"toggleFullscreen()\"\n >\n <i [c8yIcon]=\"'expand'\"></i>\n <span translate>Fullscreen</span>\n </button>\n </c8y-action-bar-item>\n}\n\n<div class=\"content-fullpage\">\n <div\n class=\"inner-scroll\"\n id=\"terminal-screen\"\n ></div>\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { EventEmitter, Injectable,
|
|
2
|
+
import { EventEmitter, Injectable, signal, computed, inject, DestroyRef, Component, Pipe, ChangeDetectorRef, Input, ChangeDetectionStrategy, Output, ViewChild, NgModule } from '@angular/core';
|
|
3
|
+
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3
4
|
import * as i1 from '@angular/router';
|
|
4
5
|
import * as i3$1 from '@c8y/client';
|
|
5
6
|
import { OperationStatus, InventoryBinaryService } from '@c8y/client';
|
|
6
7
|
import { gettext } from '@c8y/ngx-components/gettext';
|
|
7
8
|
import * as i2 from '@c8y/ngx-components';
|
|
8
|
-
import { IconDirective, C8yTranslatePipe, BottomDrawerRef, AlertService, ValidationPattern, isBinaryFile, C8yTranslateDirective, FormGroupComponent, TypeaheadComponent, ForOfDirective, ListItemComponent, HighlightComponent, FilePickerComponent, MessagesComponent, RequiredInputPlaceholderDirective,
|
|
9
|
+
import { IconDirective, SelectComponent, C8yTranslatePipe, BottomDrawerRef, AlertService, ValidationPattern, isBinaryFile, C8yTranslateDirective, FormGroupComponent, TypeaheadComponent, ForOfDirective, ListItemComponent, HighlightComponent, FilePickerComponent, MessagesComponent, RequiredInputPlaceholderDirective, Permissions, DatePipe, FilterInputComponent, ActionBarItemComponent, TabsetAriaDirective, EmptyStateComponent, hookRoute, ViewContext, DataGridService, BottomDrawerService, ModalService, BuiltInActionType, Status, TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, HelpComponent, DataGridComponent, EmptyStateContextDirective, GuideDocsComponent, GuideHrefDirective, NavigatorNode, hookNavigator, CoreModule, FormsModule as FormsModule$1 } from '@c8y/ngx-components';
|
|
10
|
+
import { EditorComponent, MonacoEditorMarkerValidatorDirective } from '@c8y/ngx-components/editor';
|
|
9
11
|
import * as i3 from '@c8y/ngx-components/repository/shared';
|
|
10
12
|
import { DeviceConfigurationOperation, RepositoryService, RepositoryType, mimeTypeToEditorLanguage, SharedRepositoryModule, RepositoryItemNameGridColumn, DescriptionGridColumn, FileGridColumn, DeviceTypeGridColumn, TypeGridColumn } from '@c8y/ngx-components/repository/shared';
|
|
11
13
|
import { NgClass, NgIf, NgFor, AsyncPipe } from '@angular/common';
|
|
12
14
|
import * as i1$1 from '@angular/forms';
|
|
13
|
-
import {
|
|
15
|
+
import { FormGroup, FormControl, ReactiveFormsModule, Validators, FormsModule } from '@angular/forms';
|
|
14
16
|
import { OperationDetailsComponent, OperationDetailsModule } from '@c8y/ngx-components/operations/operation-details';
|
|
15
17
|
import { has, uniqBy } from 'lodash-es';
|
|
16
18
|
import { saveAs } from 'file-saver';
|
|
17
19
|
import { map } from 'rxjs/operators';
|
|
18
|
-
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
19
|
-
import { EditorComponent, MonacoEditorMarkerValidatorDirective } from '@c8y/ngx-components/editor';
|
|
20
20
|
import { pipe } from 'rxjs';
|
|
21
21
|
import * as i1$3 from 'ngx-bootstrap/tabs';
|
|
22
22
|
import { TabsetComponent, TabDirective, TabsModule } from 'ngx-bootstrap/tabs';
|
|
@@ -67,6 +67,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
67
67
|
type: Injectable
|
|
68
68
|
}], ctorParameters: () => [{ type: i2.BottomDrawerService }] });
|
|
69
69
|
|
|
70
|
+
/** Shared language list for Monaco-based configuration editors (no "Auto" option). */
|
|
71
|
+
const EDITOR_LANGUAGES = [
|
|
72
|
+
{ value: 'json', label: gettext('JSON') },
|
|
73
|
+
{ value: 'javascript', label: gettext('JavaScript') },
|
|
74
|
+
{ value: 'typescript', label: gettext('TypeScript') },
|
|
75
|
+
{ value: 'css', label: gettext('CSS') },
|
|
76
|
+
{ value: 'html', label: gettext('HTML') },
|
|
77
|
+
{ value: 'yaml', label: gettext('YAML') },
|
|
78
|
+
{ value: 'markdown', label: gettext('Markdown') },
|
|
79
|
+
{ value: 'xml', label: gettext('XML') },
|
|
80
|
+
{ value: 'ini', label: gettext('INI / TOML / Properties') },
|
|
81
|
+
{ value: 'shell', label: gettext('Shell') },
|
|
82
|
+
{ value: 'plaintext', label: gettext('Plain text') }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Device text configs are plain text by default — put plaintext first in the picker.
|
|
86
|
+
const PLAINTEXT_OPTION = EDITOR_LANGUAGES[EDITOR_LANGUAGES.length - 1];
|
|
87
|
+
const TEXT_EDITOR_LANGUAGES = [
|
|
88
|
+
PLAINTEXT_OPTION,
|
|
89
|
+
...EDITOR_LANGUAGES.slice(0, -1)
|
|
90
|
+
];
|
|
70
91
|
class TextBasedConfigurationComponent {
|
|
71
92
|
constructor(route, alertService, repositoryService, deviceConfigurationService, inventoryService) {
|
|
72
93
|
this.route = route;
|
|
@@ -74,7 +95,38 @@ class TextBasedConfigurationComponent {
|
|
|
74
95
|
this.repositoryService = repositoryService;
|
|
75
96
|
this.deviceConfigurationService = deviceConfigurationService;
|
|
76
97
|
this.inventoryService = inventoryService;
|
|
77
|
-
this.reloadingConfig = false;
|
|
98
|
+
this.reloadingConfig = signal(false, ...(ngDevMode ? [{ debugName: "reloadingConfig" }] : /* istanbul ignore next */ []));
|
|
99
|
+
this.latestOperation = signal(undefined, ...(ngDevMode ? [{ debugName: "latestOperation" }] : /* istanbul ignore next */ []));
|
|
100
|
+
this.form = new FormGroup({
|
|
101
|
+
config: new FormControl(''),
|
|
102
|
+
language: new FormControl(PLAINTEXT_OPTION)
|
|
103
|
+
});
|
|
104
|
+
this.editorLanguages = TEXT_EDITOR_LANGUAGES;
|
|
105
|
+
this.editorLanguageValue = toSignal(this.form.controls.language.valueChanges, {
|
|
106
|
+
initialValue: PLAINTEXT_OPTION
|
|
107
|
+
});
|
|
108
|
+
this.editorLanguage = computed(() => this.editorLanguageValue()?.value ?? 'plaintext', ...(ngDevMode ? [{ debugName: "editorLanguage" }] : /* istanbul ignore next */ []));
|
|
109
|
+
this.savingConfig = computed(() => {
|
|
110
|
+
const operation = this.latestOperation();
|
|
111
|
+
return operation
|
|
112
|
+
? !!operation.c8y_Configuration &&
|
|
113
|
+
(operation.status === OperationStatus.PENDING ||
|
|
114
|
+
operation.status === OperationStatus.EXECUTING)
|
|
115
|
+
: false;
|
|
116
|
+
}, ...(ngDevMode ? [{ debugName: "savingConfig" }] : /* istanbul ignore next */ []));
|
|
117
|
+
// Read-only is driven through editorOptions (not the form control's disabled state) because
|
|
118
|
+
// the editor only applies the disabled state at creation time. computed() keeps the reference
|
|
119
|
+
// stable until read-only actually changes, so the editor does not re-run updateOptions() needlessly.
|
|
120
|
+
this.editorOptions = computed(() => ({
|
|
121
|
+
language: this.editorLanguage(),
|
|
122
|
+
readOnly: this.reloadingConfig() || this.savingConfig(),
|
|
123
|
+
minimap: { enabled: false },
|
|
124
|
+
scrollBeyondLastLine: false
|
|
125
|
+
}), ...(ngDevMode ? [{ debugName: "editorOptions" }] : /* istanbul ignore next */ []));
|
|
126
|
+
this.destroyRef = inject(DestroyRef);
|
|
127
|
+
}
|
|
128
|
+
get configControl() {
|
|
129
|
+
return this.form.controls.config;
|
|
78
130
|
}
|
|
79
131
|
async ngOnInit() {
|
|
80
132
|
await this.load();
|
|
@@ -86,35 +138,30 @@ class TextBasedConfigurationComponent {
|
|
|
86
138
|
this.showTextBasedConfigReload = this.deviceConfigurationService.hasAnySupportedOperation(this.device, [DeviceConfigurationOperation.SEND_CONFIG]);
|
|
87
139
|
this.showTextBasedConfigSave = this.deviceConfigurationService.hasAnySupportedOperation(this.device, [DeviceConfigurationOperation.CONFIG]);
|
|
88
140
|
if (this.device.c8y_Configuration && this.device.c8y_Configuration.config) {
|
|
89
|
-
this.
|
|
141
|
+
this.configControl.setValue(this.device.c8y_Configuration.config);
|
|
90
142
|
}
|
|
91
143
|
}
|
|
92
144
|
async loadOperation() {
|
|
93
145
|
const operation = await this.repositoryService.getLastConfigUpdateOperation(this.device.id);
|
|
94
146
|
if (operation !== null) {
|
|
95
|
-
this.reloadingConfig
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
147
|
+
this.reloadingConfig.set(!!operation.c8y_SendConfiguration &&
|
|
148
|
+
(operation.status === OperationStatus.PENDING ||
|
|
149
|
+
operation.status === OperationStatus.EXECUTING));
|
|
150
|
+
this.repositoryService
|
|
151
|
+
.observeOperation(operation)
|
|
152
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
153
|
+
.subscribe(operationUpdate => {
|
|
100
154
|
if (operationUpdate.status === OperationStatus.PENDING ||
|
|
101
155
|
operationUpdate.status === OperationStatus.EXECUTING) {
|
|
102
|
-
this.latestOperation
|
|
156
|
+
this.latestOperation.set(operationUpdate);
|
|
103
157
|
}
|
|
104
158
|
else
|
|
105
|
-
this.latestOperation
|
|
159
|
+
this.latestOperation.set(null);
|
|
106
160
|
});
|
|
107
161
|
}
|
|
108
162
|
}
|
|
109
|
-
get savingConfig() {
|
|
110
|
-
return this.latestOperation
|
|
111
|
-
? !!this.latestOperation.c8y_Configuration &&
|
|
112
|
-
(this.latestOperation.status === OperationStatus.PENDING ||
|
|
113
|
-
this.latestOperation.status === OperationStatus.EXECUTING)
|
|
114
|
-
: false;
|
|
115
|
-
}
|
|
116
163
|
async reloadConfiguration() {
|
|
117
|
-
this.reloadingConfig
|
|
164
|
+
this.reloadingConfig.set(true);
|
|
118
165
|
const operationCfg = await this.repositoryService.createTextBasedConfigurationReloadOperation(this.device);
|
|
119
166
|
try {
|
|
120
167
|
this.repositoryService.createObservedOperation(operationCfg).subscribe(operationUpdate => this.onOperationReloadSuccess(operationUpdate), operationUpdate => this.onOperationReloadError(operationUpdate), () => this.onOperationReloadComplete());
|
|
@@ -133,31 +180,31 @@ class TextBasedConfigurationComponent {
|
|
|
133
180
|
}
|
|
134
181
|
}
|
|
135
182
|
onOperationReloadSuccess(operationUpdate) {
|
|
136
|
-
this.latestOperation
|
|
183
|
+
this.latestOperation.set(operationUpdate);
|
|
137
184
|
if (operationUpdate.status === OperationStatus.PENDING) {
|
|
138
185
|
this.alertService.success(gettext('Configuration will be reloaded.'));
|
|
139
186
|
}
|
|
140
187
|
}
|
|
141
188
|
onOperationReloadError(operationUpdate) {
|
|
142
|
-
this.latestOperation
|
|
143
|
-
this.reloadingConfig
|
|
189
|
+
this.latestOperation.set(operationUpdate);
|
|
190
|
+
this.reloadingConfig.set(false);
|
|
144
191
|
}
|
|
145
192
|
async onOperationReloadComplete() {
|
|
146
193
|
await this.loadDevice();
|
|
147
|
-
this.
|
|
148
|
-
this.reloadingConfig
|
|
194
|
+
this.configControl.setValue(this.device.c8y_Configuration.config);
|
|
195
|
+
this.reloadingConfig.set(false);
|
|
149
196
|
}
|
|
150
197
|
onOperationUpdateSuccess(operationUpdate) {
|
|
151
|
-
this.latestOperation
|
|
198
|
+
this.latestOperation.set(operationUpdate);
|
|
152
199
|
if (operationUpdate.status === OperationStatus.PENDING) {
|
|
153
200
|
this.alertService.success(gettext('Configuration will be updated.'));
|
|
154
201
|
}
|
|
155
202
|
}
|
|
156
203
|
onOperationUpdateError(operationUpdate) {
|
|
157
|
-
this.latestOperation
|
|
204
|
+
this.latestOperation.set(operationUpdate);
|
|
158
205
|
}
|
|
159
206
|
onOperationUpdateComplete() {
|
|
160
|
-
this.device.c8y_Configuration.config = this.
|
|
207
|
+
this.device.c8y_Configuration.config = this.configControl.value;
|
|
161
208
|
}
|
|
162
209
|
async loadDevice() {
|
|
163
210
|
this.device = (await this.inventoryService.detail(this.device.id, {
|
|
@@ -165,11 +212,19 @@ class TextBasedConfigurationComponent {
|
|
|
165
212
|
})).data;
|
|
166
213
|
}
|
|
167
214
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TextBasedConfigurationComponent, deps: [{ token: i1.ActivatedRoute }, { token: i2.AlertService }, { token: i3.RepositoryService }, { token: DeviceConfigurationService }, { token: i3$1.InventoryService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
168
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: TextBasedConfigurationComponent, isStandalone: true, selector: "c8y-text-based-configuration", ngImport: i0, template: "<div
|
|
215
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: TextBasedConfigurationComponent, isStandalone: true, selector: "c8y-text-based-configuration", ngImport: i0, template: "<div\n class=\"d-flex d-col fit-h min-height-0\"\n [formGroup]=\"form\"\n>\n <fieldset class=\"card-block bg-level-1 fit-w\">\n <div class=\"content-flex-50 d-flex a-i-center\">\n <label\n class=\"m-r-8 flex-no-shrink\"\n for=\"textCfgEditorLanguage\"\n translate\n >\n Language\n </label>\n <c8y-select\n aria-label=\"{{ 'Editor language' | translate }}\"\n id=\"textCfgEditorLanguage\"\n [items]=\"editorLanguages\"\n formControlName=\"language\"\n ></c8y-select>\n <div class=\"m-l-auto d-flex\">\n @if (showTextBasedConfigReload) {\n <button\n class=\"btn btn-default btn-sm a-s-center m-t-8 m-b-8\"\n title=\"{{ 'Get configuration from device' | translate }}\"\n type=\"button\"\n (click)=\"reloadConfiguration()\"\n [disabled]=\"reloadingConfig() || savingConfig()\"\n >\n @if (reloadingConfig()) {\n <i\n class=\"m-r-4\"\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': reloadingConfig() }\"\n ></i>\n }\n @if (!reloadingConfig()) {\n <i\n class=\"m-r-4\"\n c8yIcon=\"download\"\n ></i>\n }\n\n {{ 'Get configuration from device' | translate }}\n </button>\n }\n </div>\n </div>\n </fieldset>\n <div class=\"p-relative flex-grow min-height-0\">\n <c8y-editor\n class=\"p-absolute top-0 left-0 fit-w fit-h\"\n formControlName=\"config\"\n [editorOptions]=\"editorOptions()\"\n ></c8y-editor>\n </div>\n @if (latestOperation() !== undefined) {\n <c8y-operation-details\n class=\"bg-level-2 p-0\"\n [operation]=\"latestOperation()\"\n ></c8y-operation-details>\n }\n @if (showTextBasedConfigSave) {\n <div class=\"card-footer flex-no-shrink fit-w separator\">\n <button\n class=\"btn btn-primary\"\n id=\"send-config-btn\"\n type=\"button\"\n (click)=\"updateConfiguration(configControl.value)\"\n [disabled]=\"reloadingConfig() || savingConfig() || !configControl.value\"\n [ngClass]=\"{ 'btn-pending': savingConfig() }\"\n >\n @if (!savingConfig()) {\n <span title=\"{{ 'Send' | translate }}\">\n {{ 'Send configuration to device' | translate }}\n </span>\n }\n @if (savingConfig()) {\n <span title=\"{{ 'Sending\u2026' | translate }}\">\n {{ 'Sending\u2026' | translate }}\n </span>\n }\n </button>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: EditorComponent, selector: "c8y-editor", inputs: ["editorOptions", "theme"], outputs: ["editorInit"] }, { kind: "component", type: OperationDetailsComponent, selector: "c8y-operation-details", inputs: ["operation"] }, { kind: "component", type: SelectComponent, selector: "c8y-select", inputs: ["placeholder", "items", "selected", "container", "multi", "canSelectWithSpace", "disabled", "autoClose", "insideClick", "required", "canDeselect", "name", "icon", "filterItems"], outputs: ["onSelect", "onDeselect", "onIconClick"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
|
|
169
216
|
}
|
|
170
217
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TextBasedConfigurationComponent, decorators: [{
|
|
171
218
|
type: Component,
|
|
172
|
-
args: [{ selector: 'c8y-text-based-configuration', imports: [
|
|
219
|
+
args: [{ selector: 'c8y-text-based-configuration', imports: [
|
|
220
|
+
IconDirective,
|
|
221
|
+
NgClass,
|
|
222
|
+
ReactiveFormsModule,
|
|
223
|
+
EditorComponent,
|
|
224
|
+
OperationDetailsComponent,
|
|
225
|
+
C8yTranslatePipe,
|
|
226
|
+
SelectComponent
|
|
227
|
+
], template: "<div\n class=\"d-flex d-col fit-h min-height-0\"\n [formGroup]=\"form\"\n>\n <fieldset class=\"card-block bg-level-1 fit-w\">\n <div class=\"content-flex-50 d-flex a-i-center\">\n <label\n class=\"m-r-8 flex-no-shrink\"\n for=\"textCfgEditorLanguage\"\n translate\n >\n Language\n </label>\n <c8y-select\n aria-label=\"{{ 'Editor language' | translate }}\"\n id=\"textCfgEditorLanguage\"\n [items]=\"editorLanguages\"\n formControlName=\"language\"\n ></c8y-select>\n <div class=\"m-l-auto d-flex\">\n @if (showTextBasedConfigReload) {\n <button\n class=\"btn btn-default btn-sm a-s-center m-t-8 m-b-8\"\n title=\"{{ 'Get configuration from device' | translate }}\"\n type=\"button\"\n (click)=\"reloadConfiguration()\"\n [disabled]=\"reloadingConfig() || savingConfig()\"\n >\n @if (reloadingConfig()) {\n <i\n class=\"m-r-4\"\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': reloadingConfig() }\"\n ></i>\n }\n @if (!reloadingConfig()) {\n <i\n class=\"m-r-4\"\n c8yIcon=\"download\"\n ></i>\n }\n\n {{ 'Get configuration from device' | translate }}\n </button>\n }\n </div>\n </div>\n </fieldset>\n <div class=\"p-relative flex-grow min-height-0\">\n <c8y-editor\n class=\"p-absolute top-0 left-0 fit-w fit-h\"\n formControlName=\"config\"\n [editorOptions]=\"editorOptions()\"\n ></c8y-editor>\n </div>\n @if (latestOperation() !== undefined) {\n <c8y-operation-details\n class=\"bg-level-2 p-0\"\n [operation]=\"latestOperation()\"\n ></c8y-operation-details>\n }\n @if (showTextBasedConfigSave) {\n <div class=\"card-footer flex-no-shrink fit-w separator\">\n <button\n class=\"btn btn-primary\"\n id=\"send-config-btn\"\n type=\"button\"\n (click)=\"updateConfiguration(configControl.value)\"\n [disabled]=\"reloadingConfig() || savingConfig() || !configControl.value\"\n [ngClass]=\"{ 'btn-pending': savingConfig() }\"\n >\n @if (!savingConfig()) {\n <span title=\"{{ 'Send' | translate }}\">\n {{ 'Send configuration to device' | translate }}\n </span>\n }\n @if (savingConfig()) {\n <span title=\"{{ 'Sending\u2026' | translate }}\">\n {{ 'Sending\u2026' | translate }}\n </span>\n }\n </button>\n </div>\n }\n</div>\n" }]
|
|
173
228
|
}], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: i2.AlertService }, { type: i3.RepositoryService }, { type: DeviceConfigurationService }, { type: i3$1.InventoryService }] });
|
|
174
229
|
|
|
175
230
|
class DeviceConfigurationGuard {
|
|
@@ -257,20 +312,10 @@ function mimeForFilename(filename) {
|
|
|
257
312
|
}
|
|
258
313
|
/** Sentinel value meaning "derive language from filename". */
|
|
259
314
|
const AUTO_LANGUAGE = 'auto';
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
{ value: 'typescript', label: gettext('TypeScript') },
|
|
265
|
-
{ value: 'css', label: gettext('CSS') },
|
|
266
|
-
{ value: 'html', label: gettext('HTML') },
|
|
267
|
-
{ value: 'yaml', label: gettext('YAML') },
|
|
268
|
-
{ value: 'markdown', label: gettext('Markdown') },
|
|
269
|
-
{ value: 'xml', label: gettext('XML') },
|
|
270
|
-
{ value: 'ini', label: gettext('INI / TOML / Properties') },
|
|
271
|
-
{ value: 'shell', label: gettext('Shell') },
|
|
272
|
-
{ value: 'plaintext', label: gettext('Plain text') }
|
|
273
|
-
];
|
|
315
|
+
const AUTO_LANGUAGE_OPTION = {
|
|
316
|
+
value: AUTO_LANGUAGE,
|
|
317
|
+
label: gettext('Auto (from filename)')
|
|
318
|
+
};
|
|
274
319
|
class ConfigurationDetailComponent {
|
|
275
320
|
constructor() {
|
|
276
321
|
this.repositoryService = inject(RepositoryService);
|
|
@@ -298,7 +343,7 @@ class ConfigurationDetailComponent {
|
|
|
298
343
|
editorFilename: new FormControl('', [
|
|
299
344
|
Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
|
|
300
345
|
]),
|
|
301
|
-
editorLanguage: new FormControl(
|
|
346
|
+
editorLanguage: new FormControl(AUTO_LANGUAGE_OPTION),
|
|
302
347
|
editorContent: new FormControl('')
|
|
303
348
|
});
|
|
304
349
|
this.configurationTypeMO = {};
|
|
@@ -321,7 +366,7 @@ class ConfigurationDetailComponent {
|
|
|
321
366
|
initialValue: ''
|
|
322
367
|
});
|
|
323
368
|
this.editorLanguageValue = toSignal(this.form.controls.editorLanguage.valueChanges, {
|
|
324
|
-
initialValue:
|
|
369
|
+
initialValue: AUTO_LANGUAGE_OPTION
|
|
325
370
|
});
|
|
326
371
|
/** User's manual pick (when not "auto") wins over auto-detection from filename. */
|
|
327
372
|
this.editorLanguage = computed(() => {
|
|
@@ -330,7 +375,7 @@ class ConfigurationDetailComponent {
|
|
|
330
375
|
? detectLanguage(this.editorFilenameValue() ?? '')
|
|
331
376
|
: picked;
|
|
332
377
|
}, ...(ngDevMode ? [{ debugName: "editorLanguage" }] : /* istanbul ignore next */ []));
|
|
333
|
-
this.editorLanguages = EDITOR_LANGUAGES;
|
|
378
|
+
this.editorLanguages = [AUTO_LANGUAGE_OPTION, ...EDITOR_LANGUAGES];
|
|
334
379
|
this.submitButtonTitle = computed(() => this.mo?.id ? gettext('Update configuration') : gettext('Add configuration'), ...(ngDevMode ? [{ debugName: "submitButtonTitle" }] : /* istanbul ignore next */ []));
|
|
335
380
|
this.form.controls.uploadChoice.valueChanges.pipe(takeUntilDestroyed()).subscribe(choice => {
|
|
336
381
|
const ctrl = this.form.controls.editorFilename;
|