@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.
Files changed (37) hide show
  1. package/fesm2022/{c8y-ngx-components-asset-property-grid.component-DhfJagxU.mjs → c8y-ngx-components-asset-property-grid.component-Ddva2QJW.mjs} +35 -19
  2. package/fesm2022/c8y-ngx-components-asset-property-grid.component-Ddva2QJW.mjs.map +1 -0
  3. package/fesm2022/c8y-ngx-components-context-dashboard.mjs +28 -14
  4. package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
  5. package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs +2 -2
  6. package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs.map +1 -1
  7. package/fesm2022/c8y-ngx-components-icon-selector.mjs +1538 -59
  8. package/fesm2022/c8y-ngx-components-icon-selector.mjs.map +1 -1
  9. package/fesm2022/c8y-ngx-components-remote-access-data.mjs +9 -2
  10. package/fesm2022/c8y-ngx-components-remote-access-data.mjs.map +1 -1
  11. package/fesm2022/c8y-ngx-components-remote-access-terminal-viewer.mjs +6 -1
  12. package/fesm2022/c8y-ngx-components-remote-access-terminal-viewer.mjs.map +1 -1
  13. package/fesm2022/c8y-ngx-components-repository-configuration.mjs +94 -49
  14. package/fesm2022/c8y-ngx-components-repository-configuration.mjs.map +1 -1
  15. package/fesm2022/c8y-ngx-components.mjs +1 -1
  16. package/locales/de.po +266 -3
  17. package/locales/es.po +266 -3
  18. package/locales/fr.po +266 -3
  19. package/locales/ja_JP.po +265 -3
  20. package/locales/ko.po +266 -3
  21. package/locales/locales.pot +266 -3
  22. package/locales/nl.po +266 -3
  23. package/locales/pl.po +266 -3
  24. package/locales/pt_BR.po +266 -3
  25. package/locales/zh_CN.po +266 -3
  26. package/locales/zh_TW.po +266 -3
  27. package/package.json +1 -1
  28. package/types/c8y-ngx-components-context-dashboard.d.ts +7 -3
  29. package/types/c8y-ngx-components-context-dashboard.d.ts.map +1 -1
  30. package/types/c8y-ngx-components-icon-selector.d.ts +286 -32
  31. package/types/c8y-ngx-components-icon-selector.d.ts.map +1 -1
  32. package/types/c8y-ngx-components-remote-access-data.d.ts +8 -1
  33. package/types/c8y-ngx-components-remote-access-data.d.ts.map +1 -1
  34. package/types/c8y-ngx-components-remote-access-terminal-viewer.d.ts.map +1 -1
  35. package/types/c8y-ngx-components-repository-configuration.d.ts +20 -10
  36. package/types/c8y-ngx-components-repository-configuration.d.ts.map +1 -1
  37. 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(this.remoteAccess.getWebSocketUri(this.deviceId, this.configurationId), 'binary');
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('Establishing Websocket connection...  \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 '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;'\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 '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;'\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, Component, Pipe, inject, ChangeDetectorRef, signal, computed, Input, ChangeDetectionStrategy, Output, ViewChild, NgModule } from '@angular/core';
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, SelectComponent, 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';
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 { FormsModule, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
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.config = this.device.c8y_Configuration.config;
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
- !!operation.c8y_SendConfiguration &&
97
- (operation.status === OperationStatus.PENDING ||
98
- operation.status === OperationStatus.EXECUTING);
99
- this.repositoryService.observeOperation(operation).subscribe(operationUpdate => {
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 = operationUpdate;
156
+ this.latestOperation.set(operationUpdate);
103
157
  }
104
158
  else
105
- this.latestOperation = null;
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 = true;
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 = operationUpdate;
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 = operationUpdate;
143
- this.reloadingConfig = false;
189
+ this.latestOperation.set(operationUpdate);
190
+ this.reloadingConfig.set(false);
144
191
  }
145
192
  async onOperationReloadComplete() {
146
193
  await this.loadDevice();
147
- this.config = this.device.c8y_Configuration.config;
148
- this.reloadingConfig = false;
194
+ this.configControl.setValue(this.device.c8y_Configuration.config);
195
+ this.reloadingConfig.set(false);
149
196
  }
150
197
  onOperationUpdateSuccess(operationUpdate) {
151
- this.latestOperation = operationUpdate;
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 = operationUpdate;
204
+ this.latestOperation.set(operationUpdate);
158
205
  }
159
206
  onOperationUpdateComplete() {
160
- this.device.c8y_Configuration.config = this.config;
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 class=\"d-flex d-col fit-h\">\n <fieldset class=\"card-block bg-level-1 fit-w\">\n <div class=\"content-flex-50\">\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=\"flex-grow\">\n <textarea\n class=\"form-control fit-h p-r-16 p-l-16\"\n [attr.aria-label]=\"'Operations' | translate\"\n [(ngModel)]=\"config\"\n [disabled]=\"reloadingConfig || savingConfig\"\n c8y-spellcheck=\"false\"\n ></textarea>\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 fit-w separator\">\n <button\n class=\"btn btn-primary\"\n id=\"send-config-btn\"\n type=\"button\"\n (click)=\"updateConfiguration(config)\"\n [disabled]=\"reloadingConfig || savingConfig || !config\"\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: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OperationDetailsComponent, selector: "c8y-operation-details", inputs: ["operation"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
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: [IconDirective, NgClass, FormsModule, OperationDetailsComponent, C8yTranslatePipe], template: "<div class=\"d-flex d-col fit-h\">\n <fieldset class=\"card-block bg-level-1 fit-w\">\n <div class=\"content-flex-50\">\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=\"flex-grow\">\n <textarea\n class=\"form-control fit-h p-r-16 p-l-16\"\n [attr.aria-label]=\"'Operations' | translate\"\n [(ngModel)]=\"config\"\n [disabled]=\"reloadingConfig || savingConfig\"\n c8y-spellcheck=\"false\"\n ></textarea>\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 fit-w separator\">\n <button\n class=\"btn btn-primary\"\n id=\"send-config-btn\"\n type=\"button\"\n (click)=\"updateConfiguration(config)\"\n [disabled]=\"reloadingConfig || savingConfig || !config\"\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" }]
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 EDITOR_LANGUAGES = [
261
- { value: AUTO_LANGUAGE, label: gettext('Auto (from filename)') },
262
- { value: 'json', label: gettext('JSON') },
263
- { value: 'javascript', label: gettext('JavaScript') },
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(EDITOR_LANGUAGES[0]),
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: EDITOR_LANGUAGES[0]
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;