@c8y/ngx-components 1024.0.0 → 1024.1.5

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 (34) hide show
  1. package/fesm2022/c8y-ngx-components-datapoint-explorer-devicemanagement.mjs +0 -1
  2. package/fesm2022/c8y-ngx-components-datapoint-explorer-devicemanagement.mjs.map +1 -1
  3. package/fesm2022/c8y-ngx-components-operations-bulk-single-operations-list.mjs +4 -6
  4. package/fesm2022/c8y-ngx-components-operations-bulk-single-operations-list.mjs.map +1 -1
  5. package/fesm2022/c8y-ngx-components-repository-configuration.mjs +493 -417
  6. package/fesm2022/c8y-ngx-components-repository-configuration.mjs.map +1 -1
  7. package/fesm2022/c8y-ngx-components-repository-shared.mjs +69 -10
  8. package/fesm2022/c8y-ngx-components-repository-shared.mjs.map +1 -1
  9. package/fesm2022/c8y-ngx-components-widgets-implementations-quick-links.mjs +53 -24
  10. package/fesm2022/c8y-ngx-components-widgets-implementations-quick-links.mjs.map +1 -1
  11. package/fesm2022/c8y-ngx-components.mjs +51 -24
  12. package/fesm2022/c8y-ngx-components.mjs.map +1 -1
  13. package/locales/de.po +42 -39
  14. package/locales/es.po +32 -29
  15. package/locales/fr.po +24 -21
  16. package/locales/ja_JP.po +47 -44
  17. package/locales/ko.po +30 -27
  18. package/locales/locales.pot +3 -0
  19. package/locales/nl.po +27 -24
  20. package/locales/pl.po +42 -39
  21. package/locales/pt_BR.po +29 -26
  22. package/locales/zh_CN.po +28 -25
  23. package/locales/zh_TW.po +38 -35
  24. package/package.json +1 -1
  25. package/types/c8y-ngx-components-datapoint-explorer-devicemanagement.d.ts.map +1 -1
  26. package/types/c8y-ngx-components-operations-bulk-single-operations-list.d.ts.map +1 -1
  27. package/types/c8y-ngx-components-repository-configuration.d.ts +30 -11
  28. package/types/c8y-ngx-components-repository-configuration.d.ts.map +1 -1
  29. package/types/c8y-ngx-components-repository-shared.d.ts +26 -2
  30. package/types/c8y-ngx-components-repository-shared.d.ts.map +1 -1
  31. package/types/c8y-ngx-components-widgets-implementations-quick-links.d.ts +18 -5
  32. package/types/c8y-ngx-components-widgets-implementations-quick-links.d.ts.map +1 -1
  33. package/types/c8y-ngx-components.d.ts +16 -5
  34. package/types/c8y-ngx-components.d.ts.map +1 -1
@@ -1,35 +1,55 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Injectable, Component, Pipe, Input, Output, ViewChild, NgModule, inject, ChangeDetectorRef, signal, computed, ChangeDetectionStrategy } from '@angular/core';
2
+ import { EventEmitter, Injectable, Component, Pipe, inject, ChangeDetectorRef, signal, computed, Input, ChangeDetectionStrategy, Output, ViewChild, NgModule } from '@angular/core';
3
3
  import * as i1 from '@angular/router';
4
- import * as i4 from '@c8y/client';
4
+ import * as i3$1 from '@c8y/client';
5
5
  import { OperationStatus, InventoryBinaryService } from '@c8y/client';
6
6
  import { gettext } from '@c8y/ngx-components/gettext';
7
7
  import * as i2 from '@c8y/ngx-components';
8
- import { IconDirective, C8yTranslatePipe, C8yTranslateDirective, FormGroupComponent, RequiredInputPlaceholderDirective, Permissions, DatePipe, FilterInputComponent, ActionBarItemComponent, TabsetAriaDirective, EmptyStateComponent, hookRoute, ViewContext, BottomDrawerRef, AlertService, ValidationPattern, isBinaryFile, TypeaheadComponent, ForOfDirective, ListItemComponent, HighlightComponent, FilePickerComponent, MessagesComponent, SelectComponent, DataGridService, BottomDrawerService, ModalService, BuiltInActionType, Status, TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, HelpComponent, DataGridComponent, EmptyStateContextDirective, GuideDocsComponent, GuideHrefDirective, NavigatorNode, hookNavigator, CoreModule, FormsModule as FormsModule$1 } 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
9
  import * as i3 from '@c8y/ngx-components/repository/shared';
10
- import { DeviceConfigurationOperation, RepositoryType, SharedRepositoryModule, RepositoryService, RepositoryItemNameGridColumn, DescriptionGridColumn, FileGridColumn, DeviceTypeGridColumn, TypeGridColumn } from '@c8y/ngx-components/repository/shared';
10
+ import { DeviceConfigurationOperation, RepositoryService, RepositoryType, mimeTypeToEditorLanguage, SharedRepositoryModule, RepositoryItemNameGridColumn, DescriptionGridColumn, FileGridColumn, DeviceTypeGridColumn, TypeGridColumn } from '@c8y/ngx-components/repository/shared';
11
11
  import { NgClass, NgIf, NgFor, AsyncPipe } from '@angular/common';
12
- import * as i4$1 from '@angular/forms';
12
+ import * as i1$1 from '@angular/forms';
13
13
  import { FormsModule, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
14
14
  import { OperationDetailsComponent, OperationDetailsModule } from '@c8y/ngx-components/operations/operation-details';
15
- import { has, cloneDeep, uniqBy } from 'lodash-es';
15
+ import { has, uniqBy } from 'lodash-es';
16
16
  import { saveAs } from 'file-saver';
17
- import * as i1$1 from 'ngx-bootstrap/modal';
18
17
  import { map } from 'rxjs/operators';
19
- import * as i1$2 from 'ngx-bootstrap/tabs';
20
- import { TabsetComponent, TabDirective, TabsModule } from 'ngx-bootstrap/tabs';
21
- import { TranslateService } from '@ngx-translate/core';
22
18
  import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
23
19
  import { EditorComponent, MonacoEditorMarkerValidatorDirective } from '@c8y/ngx-components/editor';
24
20
  import { pipe } from 'rxjs';
21
+ import * as i1$3 from 'ngx-bootstrap/tabs';
22
+ import { TabsetComponent, TabDirective, TabsModule } from 'ngx-bootstrap/tabs';
23
+ import * as i1$2 from 'ngx-bootstrap/modal';
24
+ import { TranslateService } from '@ngx-translate/core';
25
25
 
26
26
  class DeviceConfigurationService {
27
- constructor() {
27
+ constructor(bottomDrawerService) {
28
+ this.bottomDrawerService = bottomDrawerService;
28
29
  this.configurationsUpdated = new EventEmitter();
29
30
  }
30
31
  updateConfigurations(repositorySnapsOnly) {
31
32
  this.configurationsUpdated.emit(repositorySnapsOnly);
32
33
  }
34
+ /**
35
+ * Opens a component in a bottom drawer prefilled with `initialState` and resolves once the
36
+ * user saves (the drawer's `result` promise settles), or `false` if they cancel. Shared by
37
+ * the "Save to repository" and "Edit in repository" flows, which differ only in the payload.
38
+ */
39
+ async openConfigurationDrawer(component, initialState) {
40
+ try {
41
+ const drawer = this.bottomDrawerService.openDrawer(component, {
42
+ disableClickOutside: true,
43
+ closeOnNavigation: true,
44
+ initialState
45
+ });
46
+ await drawer.instance.result;
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
33
53
  hasAnySupportedOperation(mo, operation) {
34
54
  const supported = mo.c8y_SupportedOperations;
35
55
  if (!supported) {
@@ -40,12 +60,12 @@ class DeviceConfigurationService {
40
60
  }
41
61
  return supported.some(supportedOperation => operation.includes(supportedOperation));
42
62
  }
43
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DeviceConfigurationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
63
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DeviceConfigurationService, deps: [{ token: i2.BottomDrawerService }], target: i0.ɵɵFactoryTarget.Injectable }); }
44
64
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DeviceConfigurationService }); }
45
65
  }
46
66
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DeviceConfigurationService, decorators: [{
47
67
  type: Injectable
48
- }] });
68
+ }], ctorParameters: () => [{ type: i2.BottomDrawerService }] });
49
69
 
50
70
  class TextBasedConfigurationComponent {
51
71
  constructor(route, alertService, repositoryService, deviceConfigurationService, inventoryService) {
@@ -144,13 +164,13 @@ class TextBasedConfigurationComponent {
144
164
  withChildren: false
145
165
  })).data;
146
166
  }
147
- 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: i4.InventoryService }], target: i0.ɵɵFactoryTarget.Component }); }
148
- 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: i4$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: i4$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4$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" }] }); }
167
+ 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" }] }); }
149
169
  }
150
170
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: TextBasedConfigurationComponent, decorators: [{
151
171
  type: Component,
152
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" }]
153
- }], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: i2.AlertService }, { type: i3.RepositoryService }, { type: DeviceConfigurationService }, { type: i4.InventoryService }] });
173
+ }], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: i2.AlertService }, { type: i3.RepositoryService }, { type: DeviceConfigurationService }, { type: i3$1.InventoryService }] });
154
174
 
155
175
  class DeviceConfigurationGuard {
156
176
  constructor(deviceConfigurationService) {
@@ -197,91 +217,368 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
197
217
  args: [{ name: 'configurationFilterPipe' }]
198
218
  }] });
199
219
 
200
- class SaveToRepositoryComponent {
201
- constructor(modal, alertService, repositoryService) {
202
- this.modal = modal;
203
- this.alertService = alertService;
204
- this.repositoryService = repositoryService;
220
+ const EXT_LANGUAGE_MAP = {
221
+ json: 'json',
222
+ yaml: 'yaml',
223
+ yml: 'yaml',
224
+ xml: 'xml',
225
+ html: 'html',
226
+ css: 'css',
227
+ ts: 'typescript',
228
+ js: 'javascript',
229
+ md: 'markdown',
230
+ toml: 'ini',
231
+ properties: 'ini',
232
+ ini: 'ini',
233
+ conf: 'ini',
234
+ cfg: 'ini',
235
+ sh: 'shell'
236
+ };
237
+ function detectLanguage(filename) {
238
+ const ext = (filename.split('.').pop() ?? '').toLowerCase();
239
+ return EXT_LANGUAGE_MAP[ext] ?? 'plaintext';
240
+ }
241
+ const EXT_MIME_MAP = {
242
+ json: 'application/json',
243
+ xml: 'application/xml',
244
+ yaml: 'application/yaml',
245
+ yml: 'application/yaml',
246
+ js: 'application/javascript',
247
+ ts: 'application/typescript',
248
+ html: 'text/html',
249
+ css: 'text/css',
250
+ md: 'text/markdown',
251
+ sh: 'application/x-sh',
252
+ toml: 'application/toml'
253
+ };
254
+ function mimeForFilename(filename) {
255
+ const ext = (filename.split('.').pop() ?? '').toLowerCase();
256
+ return EXT_MIME_MAP[ext] ?? 'text/plain';
257
+ }
258
+ /** Sentinel value meaning "derive language from filename". */
259
+ 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
+ ];
274
+ class ConfigurationDetailComponent {
275
+ constructor() {
276
+ this.repositoryService = inject(RepositoryService);
277
+ this.bottomDrawerRef = inject(BottomDrawerRef);
278
+ this.alert = inject(AlertService);
279
+ this.inventoryBinaryService = inject(InventoryBinaryService);
280
+ this.cdr = inject(ChangeDetectorRef);
281
+ this.binary = { file: undefined, url: undefined };
282
+ this.mo = {};
283
+ this.form = new FormGroup({
284
+ version: new FormControl('', [
285
+ Validators.required,
286
+ Validators.maxLength(254),
287
+ Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
288
+ ]),
289
+ deviceType: new FormControl('', [
290
+ Validators.maxLength(254),
291
+ Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
292
+ ]),
293
+ description: new FormControl('', [
294
+ Validators.maxLength(254),
295
+ Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
296
+ ]),
297
+ uploadChoice: new FormControl('uploadBinary'),
298
+ editorFilename: new FormControl('', [
299
+ Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
300
+ ]),
301
+ editorLanguage: new FormControl(EDITOR_LANGUAGES[0]),
302
+ editorContent: new FormControl('')
303
+ });
304
+ this.configurationTypeMO = {};
305
+ this.saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
306
+ this.pattern = signal('', ...(ngDevMode ? [{ debugName: "pattern" }] : /* istanbul ignore next */ []));
307
+ this.isExternalUrl = signal(false, ...(ngDevMode ? [{ debugName: "isExternalUrl" }] : /* istanbul ignore next */ []));
308
+ this.ValidationPattern = ValidationPattern;
309
+ this.textForConfigurationUrlPopover = gettext(`Path for binaries can vary depending on device agent implementation, for example:
310
+ /configuration/binaries/configuration1.bin
311
+ https://configuration/binary/123
312
+ ftp://configuration/binary/123.tar.gz
313
+ Configurations with external URLs only work with the configuration typed devices (file-based configuration), not with devices with a legacy configuration.
314
+ `);
205
315
  this.result = new Promise((resolve, reject) => {
206
316
  this._save = resolve;
207
317
  this._cancel = reject;
208
318
  });
319
+ this.settled = false;
320
+ this.editorFilenameValue = toSignal(this.form.controls.editorFilename.valueChanges, {
321
+ initialValue: ''
322
+ });
323
+ this.editorLanguageValue = toSignal(this.form.controls.editorLanguage.valueChanges, {
324
+ initialValue: EDITOR_LANGUAGES[0]
325
+ });
326
+ /** User's manual pick (when not "auto") wins over auto-detection from filename. */
327
+ this.editorLanguage = computed(() => {
328
+ const picked = this.editorLanguageValue()?.value;
329
+ return !picked || picked === AUTO_LANGUAGE
330
+ ? detectLanguage(this.editorFilenameValue() ?? '')
331
+ : picked;
332
+ }, ...(ngDevMode ? [{ debugName: "editorLanguage" }] : /* istanbul ignore next */ []));
333
+ this.editorLanguages = EDITOR_LANGUAGES;
334
+ this.submitButtonTitle = computed(() => this.mo?.id ? gettext('Update configuration') : gettext('Add configuration'), ...(ngDevMode ? [{ debugName: "submitButtonTitle" }] : /* istanbul ignore next */ []));
335
+ this.form.controls.uploadChoice.valueChanges.pipe(takeUntilDestroyed()).subscribe(choice => {
336
+ const ctrl = this.form.controls.editorFilename;
337
+ if (choice === 'editInline') {
338
+ ctrl.addValidators(Validators.required);
339
+ }
340
+ else {
341
+ ctrl.removeValidators(Validators.required);
342
+ }
343
+ ctrl.updateValueAndValidity();
344
+ });
345
+ // Drawer can be closed externally (Escape, navigation, programmatic close).
346
+ // Make sure the result promise is always settled, otherwise callers awaiting it hang.
347
+ this.bottomDrawerRef.onClosed$.pipe(takeUntilDestroyed()).subscribe(() => {
348
+ if (!this.settled) {
349
+ this.settled = true;
350
+ this._cancel('Drawer closed');
351
+ }
352
+ });
209
353
  }
210
- async save() {
211
- {
212
- try {
213
- const configSnapshotData = {
214
- selected: {
215
- configurationType: this.configSnapshot.configurationType
216
- },
217
- version: this.configSnapshot.name,
218
- deviceType: this.configSnapshot.deviceType,
219
- description: this.configSnapshot.description,
220
- binary: {
221
- file: new File([this.configSnapshot.binary], this.configSnapshot.name)
222
- }
223
- };
224
- await this.repositoryService.create(configSnapshotData, RepositoryType.CONFIGURATION);
225
- this.alertService.success(gettext('Configuration saved.'));
226
- this._save();
354
+ async ngOnInit() {
355
+ this.configs = await this.repositoryService.listRepositoryEntries(RepositoryType.CONFIGURATION);
356
+ this.setPipe('');
357
+ // Prefill when editing an existing entry (mo.id) or when the caller supplies
358
+ // content to seed a new entry — e.g. saving a device snapshot to the repository.
359
+ const shouldPrefill = !!(this.mo?.id || this.binary?.file || this.version);
360
+ if (shouldPrefill) {
361
+ this.configurationTypeMO = this.mo;
362
+ const hasExternalUrl = !!this.binary?.url && !this.inventoryBinaryService.getIdFromUrl(this.binary.url);
363
+ if (hasExternalUrl) {
364
+ this.isExternalUrl.set(true);
365
+ this.form.patchValue({
366
+ version: this.version ?? '',
367
+ deviceType: this.deviceType ?? '',
368
+ description: this.description ?? '',
369
+ uploadChoice: 'uploadUrl'
370
+ });
227
371
  }
228
- catch (ex) {
229
- this.alertService.addServerFailure(ex);
372
+ else {
373
+ const patchValues = {
374
+ version: this.version ?? '',
375
+ deviceType: this.deviceType ?? '',
376
+ description: this.description ?? '',
377
+ uploadChoice: 'editInline'
378
+ };
379
+ if (this.binary?.file) {
380
+ this.existingBinary = this.binary.file;
381
+ // Decide from the content, not the declared MIME type: platform-served binaries
382
+ // are often labelled text/plain, and their bytes must never open in the editor.
383
+ if (await isBinaryFile(this.binary.file, { ignoreMimeType: true })) {
384
+ patchValues.uploadChoice = 'uploadBinary';
385
+ }
386
+ else {
387
+ patchValues.editorContent = await this.binary.file.text();
388
+ patchValues.editorFilename = this.binary.file.name;
389
+ }
390
+ }
391
+ this.form.patchValue(patchValues);
230
392
  }
393
+ this.form.markAsDirty();
231
394
  }
395
+ this.cdr.markForCheck();
232
396
  }
233
- close() {
234
- this._cancel();
235
- this.modal.hide();
236
- }
237
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SaveToRepositoryComponent, deps: [{ token: i1$1.BsModalRef }, { token: i2.AlertService }, { token: i3.RepositoryService }], target: i0.ɵɵFactoryTarget.Component }); }
238
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: SaveToRepositoryComponent, isStandalone: true, selector: "c8y-save-config-to-configuration-repository", ngImport: i0, template: "<div class=\"modal-header dialog-header\">\n <i c8yIcon=\"gears\"></i>\n <h4 id=\"modal-title\" translate>\n Save configuration\n </h4>\n</div>\n<div class=\"modal-body\" id=\"modal-body\">\n <form #saveConfigurationSnapshot=\"ngForm\" class=\"p-t-24\">\n <c8y-form-group>\n <label translate for=\"name\">Name</label>\n <input\n id=\"name\"\n type=\"text\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"name\"\n [(ngModel)]=\"configSnapshot.name\"\n required\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"deviceType\">Device type</label>\n <input\n id=\"deviceType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"deviceType\"\n [(ngModel)]=\"configSnapshot.deviceType\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"description\">Description</label>\n <input\n type=\"text\"\n id=\"description\"\n class=\"form-control\"\n maxlength=\"254\"\n autocomplete=\"off\"\n name=\"description\"\n [(ngModel)]=\"configSnapshot.description\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"configurationType\">Configuration type</label>\n <input\n id=\"configurationType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"configurationType\"\n [(ngModel)]=\"configSnapshot.configurationType\"\n />\n </c8y-form-group>\n </form>\n</div>\n<div class=\"modal-footer\">\n <button title=\"{{ 'Cancel' | translate }}\" class=\"btn btn-default\" (click)=\"close()\" translate>\n Cancel\n </button>\n\n <button\n title=\"{{ 'Save configuration to repository' | translate }}\"\n class=\"btn btn-primary\"\n (click)=\"save()\"\n [disabled]=\"saveConfigurationSnapshot.form.invalid\"\n translate\n >\n Save\n </button>\n</div>\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4$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: i4$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i4$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
239
- }
240
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SaveToRepositoryComponent, decorators: [{
241
- type: Component,
242
- args: [{ selector: 'c8y-save-config-to-configuration-repository', imports: [
243
- IconDirective,
244
- C8yTranslateDirective,
245
- FormsModule,
246
- FormGroupComponent,
247
- RequiredInputPlaceholderDirective,
248
- C8yTranslatePipe
249
- ], template: "<div class=\"modal-header dialog-header\">\n <i c8yIcon=\"gears\"></i>\n <h4 id=\"modal-title\" translate>\n Save configuration\n </h4>\n</div>\n<div class=\"modal-body\" id=\"modal-body\">\n <form #saveConfigurationSnapshot=\"ngForm\" class=\"p-t-24\">\n <c8y-form-group>\n <label translate for=\"name\">Name</label>\n <input\n id=\"name\"\n type=\"text\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"name\"\n [(ngModel)]=\"configSnapshot.name\"\n required\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"deviceType\">Device type</label>\n <input\n id=\"deviceType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"deviceType\"\n [(ngModel)]=\"configSnapshot.deviceType\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"description\">Description</label>\n <input\n type=\"text\"\n id=\"description\"\n class=\"form-control\"\n maxlength=\"254\"\n autocomplete=\"off\"\n name=\"description\"\n [(ngModel)]=\"configSnapshot.description\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"configurationType\">Configuration type</label>\n <input\n id=\"configurationType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"configurationType\"\n [(ngModel)]=\"configSnapshot.configurationType\"\n />\n </c8y-form-group>\n </form>\n</div>\n<div class=\"modal-footer\">\n <button title=\"{{ 'Cancel' | translate }}\" class=\"btn btn-default\" (click)=\"close()\" translate>\n Cancel\n </button>\n\n <button\n title=\"{{ 'Save configuration to repository' | translate }}\"\n class=\"btn btn-primary\"\n (click)=\"save()\"\n [disabled]=\"saveConfigurationSnapshot.form.invalid\"\n translate\n >\n Save\n </button>\n</div>\n" }]
250
- }], ctorParameters: () => [{ type: i1$1.BsModalRef }, { type: i2.AlertService }, { type: i3.RepositoryService }] });
251
-
252
- class SourceCodePreviewComponent {
253
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SourceCodePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
254
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: SourceCodePreviewComponent, isStandalone: true, selector: "c8y-source-code-preview", inputs: { isDisabled: "isDisabled", text: "text" }, ngImport: i0, template: "<textarea\n [disabled]=\"isDisabled\"\n class=\"text-monospace form-control no-resize flex-grow\"\n rows=\"4\"\n >{{ text }}</textarea\n>\n" }); }
255
- }
256
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SourceCodePreviewComponent, decorators: [{
257
- type: Component,
258
- args: [{ selector: 'c8y-source-code-preview', template: "<textarea\n [disabled]=\"isDisabled\"\n class=\"text-monospace form-control no-resize flex-grow\"\n rows=\"4\"\n >{{ text }}</textarea\n>\n" }]
259
- }], propDecorators: { isDisabled: [{
260
- type: Input
261
- }], text: [{
262
- type: Input
263
- }] } });
264
-
265
- class ConfigurationPreviewComponent {
266
- set configurationType(type) {
267
- this._configurationType = type;
268
- this.setOperation(type);
397
+ cancel() {
398
+ this.settled = true;
399
+ this._cancel('User canceled');
400
+ this.bottomDrawerRef.close();
269
401
  }
270
- get configurationType() {
271
- return this._configurationType;
402
+ setPipe(filterStr) {
403
+ this.pattern.set(filterStr);
404
+ this.filterPipe = pipe(map(data => uniqBy(data, 'configurationType')), map(data => data.filter(mo => mo.configurationType &&
405
+ mo.configurationType.toLowerCase().indexOf(filterStr.toLowerCase()) > -1)));
272
406
  }
273
- constructor(deviceConfigurationService, operationRealtime, bsModal, user, appState, repositoryService, operationService, alertService) {
274
- this.deviceConfigurationService = deviceConfigurationService;
275
- this.operationRealtime = operationRealtime;
276
- this.bsModal = bsModal;
277
- this.user = user;
278
- this.appState = appState;
279
- this.repositoryService = repositoryService;
280
- this.operationService = operationService;
281
- this.alertService = alertService;
282
- this.isLegacy = false;
283
- this.canCallAction = true;
284
- this.deviceConfigurationOperation = DeviceConfigurationOperation;
407
+ onUploadChoiceChange(choice) {
408
+ this.form.controls.uploadChoice.setValue(choice);
409
+ this.form.markAsDirty();
410
+ }
411
+ onFileBinaryChange(file) {
412
+ this.binary = { ...this.binary, file };
413
+ this.form.markAsDirty();
414
+ this.cdr.markForCheck();
415
+ }
416
+ onFileUrlChange(url) {
417
+ this.binary = { ...this.binary, url };
418
+ this.form.markAsDirty();
419
+ this.cdr.markForCheck();
420
+ }
421
+ onTextFilePicked({ name, content }) {
422
+ this.form.patchValue({
423
+ uploadChoice: 'editInline',
424
+ editorFilename: name,
425
+ editorContent: content
426
+ });
427
+ this.form.markAsDirty();
428
+ this.cdr.markForCheck();
429
+ }
430
+ get isSaveDisabled() {
431
+ if (this.saving() || !this.form.valid || this.form.pristine) {
432
+ return true;
433
+ }
434
+ const { uploadChoice, editorFilename, editorContent } = this.form.value;
435
+ if (uploadChoice === 'uploadBinary') {
436
+ return !this.binary?.file;
437
+ }
438
+ if (uploadChoice === 'uploadUrl') {
439
+ return !this.binary?.url?.trim();
440
+ }
441
+ if (uploadChoice === 'editInline') {
442
+ return !editorFilename?.trim() || !editorContent;
443
+ }
444
+ return false;
445
+ }
446
+ async save() {
447
+ if (this.isSaveDisabled) {
448
+ return;
449
+ }
450
+ try {
451
+ this.saving.set(true);
452
+ const { version, deviceType, description, uploadChoice, editorFilename, editorContent } = this.form.value;
453
+ let binary = { ...this.binary };
454
+ if (uploadChoice === 'uploadUrl') {
455
+ binary = { url: this.binary?.url };
456
+ }
457
+ else if (uploadChoice === 'editInline') {
458
+ binary = {
459
+ file: new File([editorContent], editorFilename, { type: mimeForFilename(editorFilename) })
460
+ };
461
+ }
462
+ else if (this.existingBinary === this.binary?.file && this.binary?.url) {
463
+ // Editing an entry whose binary is unchanged: reuse the persisted url instead of
464
+ // re-uploading. Only valid when a url exists — a new entry (e.g. from a device
465
+ // snapshot) has no persisted binary, so its file must still be uploaded.
466
+ binary = { ...this.binary, file: undefined };
467
+ }
468
+ await this.repositoryService.create({
469
+ version,
470
+ description,
471
+ binary,
472
+ deviceType,
473
+ configurationType: this.configurationTypeMO?.configurationType
474
+ }, RepositoryType.CONFIGURATION, this.mo);
475
+ this.alert.success(this.mo?.id ? gettext('Configuration updated.') : gettext('Configuration created.'));
476
+ this.settled = true;
477
+ this._save();
478
+ this.bottomDrawerRef.close();
479
+ }
480
+ catch (ex) {
481
+ // Keep the drawer open so the user can fix and retry.
482
+ // Do NOT settle the result promise here.
483
+ this.alert.addServerFailure(ex);
484
+ }
485
+ finally {
486
+ this.saving.set(false);
487
+ }
488
+ }
489
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
490
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ConfigurationDetailComponent, isStandalone: true, selector: "c8y-configuration-detail", inputs: { version: "version", description: "description", binary: "binary", deviceType: "deviceType", mo: "mo" }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "<div class=\"card-header j-c-center separator flex-no-shrink p-24\">\n <i\n class=\"m-r-8\"\n [c8yIcon]=\"'cogs'\"\n aria-hidden=\"true\"\n ></i>\n <h4 id=\"drawerTitle\">\n @if (mo?.id) {\n {{ 'Update configuration' | translate }}\n } @else {\n {{ 'Add configuration' | translate }}\n }\n </h4>\n</div>\n\n<form\n class=\"d-contents\"\n [formGroup]=\"form\"\n (ngSubmit)=\"save()\"\n>\n <div\n class=\"card-body inner-scroll p-24\"\n [class.d-col]=\"form.value.uploadChoice === 'editInline'\"\n >\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confVersion\"\n translate\n >\n Name\n </label>\n <input\n class=\"form-control\"\n id=\"confVersion\"\n placeholder=\"{{ 'e.g.' | translate }} hosts\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"version\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDeviceType\"\n translate\n >\n Device type\n </label>\n <input\n class=\"form-control\"\n id=\"confDeviceType\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"deviceType\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDescription\"\n translate\n >\n Description\n </label>\n <input\n class=\"form-control\"\n id=\"confDescription\"\n placeholder=\"{{ 'e.g.' | translate }} Host configuration\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"description\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label translate>Configuration type</label>\n <c8y-typeahead\n placeholder=\"{{ 'e.g.' | translate }} ssh\"\n name=\"confType\"\n data-cy=\"conf-detail--configuration-type\"\n [ngModel]=\"configurationTypeMO\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"configurationTypeMO = $event\"\n maxlength=\"254\"\n (onSearch)=\"setPipe($event)\"\n displayProperty=\"configurationType\"\n >\n @let patternStr = pattern();\n <c8y-li\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n *c8yFor=\"let config of configs; pipe: filterPipe; notFound: notFoundTemplate\"\n (click)=\"configurationTypeMO = config; setPipe('')\"\n [active]=\"configurationTypeMO === config\"\n >\n <c8y-highlight\n [text]=\"config.configurationType || '--'\"\n [pattern]=\"patternStr\"\n ></c8y-highlight>\n </c8y-li>\n <ng-template #notFoundTemplate>\n @if (patternStr.length > 0) {\n <c8y-li class=\"bg-level-2 p-8\">\n <span translate>No match found.</span>\n <button\n class=\"btn btn-primary btn-xs m-l-8\"\n title=\"{{ 'Add new`configuration type`' | translate }}\"\n type=\"button\"\n translate\n >\n Add new`configuration type`\n </button>\n </c8y-li>\n }\n </ng-template>\n </c8y-typeahead>\n </c8y-form-group>\n </div>\n\n <fieldset\n class=\"d-col\"\n [class.flex-grow]=\"form.value.uploadChoice === 'editInline'\"\n [class.min-height-0]=\"form.value.uploadChoice === 'editInline'\"\n >\n <legend\n class=\"legend form-block m-t-40\"\n translate\n >\n Configuration file\n </legend>\n\n @if (isExternalUrl()) {\n <p\n class=\"text-muted m-b-8\"\n translate\n >\n Content is sourced from a remote URL and cannot be edited inline.\n </p>\n <c8y-form-group>\n <label\n for=\"confFileUrl\"\n translate\n >\n File URL\n </label>\n <input\n class=\"form-control\"\n id=\"confFileUrl\"\n placeholder=\"https://...\"\n type=\"url\"\n autocomplete=\"off\"\n [ngModel]=\"binary.url\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"binary = { url: $event }; form.markAsDirty()\"\n />\n </c8y-form-group>\n } @else {\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n [allowedUploadChoices]=\"['uploadBinary', 'uploadUrl', 'editInline']\"\n [uploadChoice]=\"form.controls.uploadChoice.value\"\n (uploadChoiceChange)=\"onUploadChoiceChange($event)\"\n [fileUrl]=\"binary.url\"\n (fileUrlChange)=\"onFileUrlChange($event)\"\n [fileBinary]=\"binary.file\"\n (fileBinaryChange)=\"onFileBinaryChange($event)\"\n (textFilePicked)=\"onTextFilePicked($event)\"\n [fileUrlPopover]=\"textForConfigurationUrlPopover\"\n >\n <ng-container c8yInlineEditor>\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-9\">\n <label\n for=\"confEditorFilename\"\n translate\n >\n Filename\n </label>\n <input\n class=\"form-control\"\n id=\"confEditorFilename\"\n placeholder=\"config.json\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"editorFilename\"\n />\n </c8y-form-group>\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confEditorLanguage\"\n translate\n >\n Language\n </label>\n <c8y-select\n aria-label=\"{{ 'Editor language' | translate }}\"\n id=\"confEditorLanguage\"\n [items]=\"editorLanguages\"\n formControlName=\"editorLanguage\"\n ></c8y-select>\n </c8y-form-group>\n </div>\n <c8y-form-group class=\"d-col flex-grow min-height-0 m-b-0\">\n <label\n for=\"confEditorContent\"\n translate\n >\n Content\n </label>\n <c8y-editor\n class=\"flex-grow\"\n style=\"min-height: 95px\"\n id=\"confEditorContent\"\n formControlName=\"editorContent\"\n monacoEditorMarkerValidator\n [editorOptions]=\"{ language: editorLanguage() }\"\n ></c8y-editor>\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </ng-container>\n </c8y-file-picker>\n }\n </fieldset>\n </div>\n\n <div class=\"card-footer j-c-between separator flex-no-shrink p-24\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"cancel()\"\n [disabled]=\"saving()\"\n >\n <span translate>Cancel</span>\n </button>\n <button\n class=\"btn btn-primary\"\n [class.btn-pending]=\"saving()\"\n [title]=\"submitButtonTitle() | translate\"\n type=\"submit\"\n [disabled]=\"isSaveDisabled\"\n >\n {{ submitButtonTitle() | translate }}\n </button>\n </div>\n</form>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { 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: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: TypeaheadComponent, selector: "c8y-typeahead", inputs: ["required", "maxlength", "disabled", "allowFreeEntries", "placeholder", "displayProperty", "icon", "name", "autoClose", "hideNew", "container", "selected", "title", "highlightFirstItem"], outputs: ["onSearch", "onIconClick"] }, { kind: "directive", type: ForOfDirective, selector: "[c8yFor]", inputs: ["c8yForOf", "c8yForLoadMore", "c8yForPipe", "c8yForNotFound", "c8yForMaxIterations", "c8yForLoadingTemplate", "c8yForLoadNextLabel", "c8yForLoadingLabel", "c8yForRealtime", "c8yForRealtimeOptions", "c8yForComparator", "c8yForEnableVirtualScroll", "c8yForVirtualScrollElementSize", "c8yForVirtualScrollStrategy", "c8yForVirtualScrollContainerHeight"], outputs: ["c8yForCount", "c8yForChange", "c8yForLoadMoreComponent"] }, { kind: "component", type: ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: HighlightComponent, selector: "c8y-highlight", inputs: ["pattern", "text", "elementClass", "shouldTrimPattern"] }, { kind: "component", type: FilePickerComponent, selector: "c8y-file-picker", inputs: ["maxAllowedFiles", "uploadChoice", "allowedUploadChoices", "fileUrl", "fileBinary", "config", "filePickerIndex", "fileUrlPopover", "editInlineConfirmTitle", "editInlineConfirmMessage"], outputs: ["onFilesPicked", "uploadChoiceChange", "fileBinaryChange", "fileUrlChange", "textFilePicked"] }, { kind: "component", type: EditorComponent, selector: "c8y-editor", inputs: ["editorOptions", "theme"], outputs: ["editorInit"] }, { kind: "directive", type: MonacoEditorMarkerValidatorDirective, selector: "c8y-editor [monacoEditorMarkerValidator]" }, { kind: "component", type: MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage", "additionalMessages"] }, { kind: "directive", type: RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { 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" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
491
+ }
492
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationDetailComponent, decorators: [{
493
+ type: Component,
494
+ args: [{ selector: 'c8y-configuration-detail', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-contents' }, imports: [
495
+ ReactiveFormsModule,
496
+ FormsModule,
497
+ IconDirective,
498
+ C8yTranslateDirective,
499
+ FormGroupComponent,
500
+ TypeaheadComponent,
501
+ ForOfDirective,
502
+ ListItemComponent,
503
+ HighlightComponent,
504
+ FilePickerComponent,
505
+ NgClass,
506
+ C8yTranslatePipe,
507
+ EditorComponent,
508
+ MonacoEditorMarkerValidatorDirective,
509
+ MessagesComponent,
510
+ RequiredInputPlaceholderDirective,
511
+ SelectComponent
512
+ ], template: "<div class=\"card-header j-c-center separator flex-no-shrink p-24\">\n <i\n class=\"m-r-8\"\n [c8yIcon]=\"'cogs'\"\n aria-hidden=\"true\"\n ></i>\n <h4 id=\"drawerTitle\">\n @if (mo?.id) {\n {{ 'Update configuration' | translate }}\n } @else {\n {{ 'Add configuration' | translate }}\n }\n </h4>\n</div>\n\n<form\n class=\"d-contents\"\n [formGroup]=\"form\"\n (ngSubmit)=\"save()\"\n>\n <div\n class=\"card-body inner-scroll p-24\"\n [class.d-col]=\"form.value.uploadChoice === 'editInline'\"\n >\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confVersion\"\n translate\n >\n Name\n </label>\n <input\n class=\"form-control\"\n id=\"confVersion\"\n placeholder=\"{{ 'e.g.' | translate }} hosts\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"version\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDeviceType\"\n translate\n >\n Device type\n </label>\n <input\n class=\"form-control\"\n id=\"confDeviceType\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"deviceType\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDescription\"\n translate\n >\n Description\n </label>\n <input\n class=\"form-control\"\n id=\"confDescription\"\n placeholder=\"{{ 'e.g.' | translate }} Host configuration\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"description\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label translate>Configuration type</label>\n <c8y-typeahead\n placeholder=\"{{ 'e.g.' | translate }} ssh\"\n name=\"confType\"\n data-cy=\"conf-detail--configuration-type\"\n [ngModel]=\"configurationTypeMO\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"configurationTypeMO = $event\"\n maxlength=\"254\"\n (onSearch)=\"setPipe($event)\"\n displayProperty=\"configurationType\"\n >\n @let patternStr = pattern();\n <c8y-li\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n *c8yFor=\"let config of configs; pipe: filterPipe; notFound: notFoundTemplate\"\n (click)=\"configurationTypeMO = config; setPipe('')\"\n [active]=\"configurationTypeMO === config\"\n >\n <c8y-highlight\n [text]=\"config.configurationType || '--'\"\n [pattern]=\"patternStr\"\n ></c8y-highlight>\n </c8y-li>\n <ng-template #notFoundTemplate>\n @if (patternStr.length > 0) {\n <c8y-li class=\"bg-level-2 p-8\">\n <span translate>No match found.</span>\n <button\n class=\"btn btn-primary btn-xs m-l-8\"\n title=\"{{ 'Add new`configuration type`' | translate }}\"\n type=\"button\"\n translate\n >\n Add new`configuration type`\n </button>\n </c8y-li>\n }\n </ng-template>\n </c8y-typeahead>\n </c8y-form-group>\n </div>\n\n <fieldset\n class=\"d-col\"\n [class.flex-grow]=\"form.value.uploadChoice === 'editInline'\"\n [class.min-height-0]=\"form.value.uploadChoice === 'editInline'\"\n >\n <legend\n class=\"legend form-block m-t-40\"\n translate\n >\n Configuration file\n </legend>\n\n @if (isExternalUrl()) {\n <p\n class=\"text-muted m-b-8\"\n translate\n >\n Content is sourced from a remote URL and cannot be edited inline.\n </p>\n <c8y-form-group>\n <label\n for=\"confFileUrl\"\n translate\n >\n File URL\n </label>\n <input\n class=\"form-control\"\n id=\"confFileUrl\"\n placeholder=\"https://...\"\n type=\"url\"\n autocomplete=\"off\"\n [ngModel]=\"binary.url\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"binary = { url: $event }; form.markAsDirty()\"\n />\n </c8y-form-group>\n } @else {\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n [allowedUploadChoices]=\"['uploadBinary', 'uploadUrl', 'editInline']\"\n [uploadChoice]=\"form.controls.uploadChoice.value\"\n (uploadChoiceChange)=\"onUploadChoiceChange($event)\"\n [fileUrl]=\"binary.url\"\n (fileUrlChange)=\"onFileUrlChange($event)\"\n [fileBinary]=\"binary.file\"\n (fileBinaryChange)=\"onFileBinaryChange($event)\"\n (textFilePicked)=\"onTextFilePicked($event)\"\n [fileUrlPopover]=\"textForConfigurationUrlPopover\"\n >\n <ng-container c8yInlineEditor>\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-9\">\n <label\n for=\"confEditorFilename\"\n translate\n >\n Filename\n </label>\n <input\n class=\"form-control\"\n id=\"confEditorFilename\"\n placeholder=\"config.json\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"editorFilename\"\n />\n </c8y-form-group>\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confEditorLanguage\"\n translate\n >\n Language\n </label>\n <c8y-select\n aria-label=\"{{ 'Editor language' | translate }}\"\n id=\"confEditorLanguage\"\n [items]=\"editorLanguages\"\n formControlName=\"editorLanguage\"\n ></c8y-select>\n </c8y-form-group>\n </div>\n <c8y-form-group class=\"d-col flex-grow min-height-0 m-b-0\">\n <label\n for=\"confEditorContent\"\n translate\n >\n Content\n </label>\n <c8y-editor\n class=\"flex-grow\"\n style=\"min-height: 95px\"\n id=\"confEditorContent\"\n formControlName=\"editorContent\"\n monacoEditorMarkerValidator\n [editorOptions]=\"{ language: editorLanguage() }\"\n ></c8y-editor>\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </ng-container>\n </c8y-file-picker>\n }\n </fieldset>\n </div>\n\n <div class=\"card-footer j-c-between separator flex-no-shrink p-24\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"cancel()\"\n [disabled]=\"saving()\"\n >\n <span translate>Cancel</span>\n </button>\n <button\n class=\"btn btn-primary\"\n [class.btn-pending]=\"saving()\"\n [title]=\"submitButtonTitle() | translate\"\n type=\"submit\"\n [disabled]=\"isSaveDisabled\"\n >\n {{ submitButtonTitle() | translate }}\n </button>\n </div>\n</form>\n" }]
513
+ }], ctorParameters: () => [], propDecorators: { version: [{
514
+ type: Input
515
+ }], description: [{
516
+ type: Input
517
+ }], binary: [{
518
+ type: Input
519
+ }], deviceType: [{
520
+ type: Input
521
+ }], mo: [{
522
+ type: Input
523
+ }] } });
524
+
525
+ class SourceCodePreviewComponent {
526
+ constructor() {
527
+ // Kept as a stable reference — a fresh object every change detection cycle would make the
528
+ // editor re-run updateOptions() needlessly. Only rebuilt when the relevant inputs change.
529
+ this.editorOptions = this.buildEditorOptions();
530
+ this.cdr = inject(ChangeDetectorRef);
531
+ }
532
+ ngOnChanges(changes) {
533
+ if (changes.isDisabled || changes.binaryType) {
534
+ this.editorOptions = this.buildEditorOptions();
535
+ }
536
+ }
537
+ onEditorInit() {
538
+ this.cdr.detectChanges();
539
+ }
540
+ buildEditorOptions() {
541
+ return {
542
+ readOnly: this.isDisabled,
543
+ language: mimeTypeToEditorLanguage(this.binaryType),
544
+ minimap: { enabled: false },
545
+ scrollBeyondLastLine: false
546
+ };
547
+ }
548
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SourceCodePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
549
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: SourceCodePreviewComponent, isStandalone: true, selector: "c8y-source-code-preview", inputs: { isDisabled: "isDisabled", text: "text", binaryType: "binaryType" }, usesOnChanges: true, ngImport: i0, template: "<c8y-editor\n class=\"flex-grow\"\n [ngModel]=\"text\"\n [editorOptions]=\"editorOptions\"\n (editorInit)=\"onEditorInit()\"\n></c8y-editor>\n", dependencies: [{ kind: "component", type: EditorComponent, selector: "c8y-editor", inputs: ["editorOptions", "theme"], outputs: ["editorInit"] }, { kind: "ngmodule", type: FormsModule }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
550
+ }
551
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SourceCodePreviewComponent, decorators: [{
552
+ type: Component,
553
+ args: [{ selector: 'c8y-source-code-preview', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [EditorComponent, FormsModule], template: "<c8y-editor\n class=\"flex-grow\"\n [ngModel]=\"text\"\n [editorOptions]=\"editorOptions\"\n (editorInit)=\"onEditorInit()\"\n></c8y-editor>\n" }]
554
+ }], propDecorators: { isDisabled: [{
555
+ type: Input
556
+ }], text: [{
557
+ type: Input
558
+ }], binaryType: [{
559
+ type: Input
560
+ }] } });
561
+
562
+ class ConfigurationPreviewComponent {
563
+ set configurationType(type) {
564
+ this._configurationType = type;
565
+ this.setOperation(type);
566
+ }
567
+ get configurationType() {
568
+ return this._configurationType;
569
+ }
570
+ constructor(deviceConfigurationService, operationRealtime, user, appState, repositoryService, operationService, alertService) {
571
+ this.deviceConfigurationService = deviceConfigurationService;
572
+ this.operationRealtime = operationRealtime;
573
+ this.user = user;
574
+ this.appState = appState;
575
+ this.repositoryService = repositoryService;
576
+ this.operationService = operationService;
577
+ this.alertService = alertService;
578
+ this.isLegacy = false;
579
+ this.editInRepository = new EventEmitter();
580
+ this.canCallAction = true;
581
+ this.deviceConfigurationOperation = DeviceConfigurationOperation;
285
582
  }
286
583
  async ngOnInit() {
287
584
  this.setCanCallAction();
@@ -365,23 +662,22 @@ class ConfigurationPreviewComponent {
365
662
  saveAs(blob, fileName);
366
663
  }
367
664
  async saveToRepository() {
368
- const initialState = {
369
- configSnapshot: cloneDeep(this.configSnapshot)
370
- };
371
- const modal = this.bsModal.show(SaveToRepositoryComponent, {
372
- class: 'modal-sm',
373
- ariaDescribedby: 'modal-body',
374
- ariaLabelledBy: 'modal-title',
375
- initialState,
376
- ignoreBackdropClick: true
377
- }).content;
378
- try {
379
- await modal.result;
665
+ const snapshot = this.configSnapshot;
666
+ // Prefer the original binary (real bytes + filename) so the drawer can tell text
667
+ // from binary: text opens in the inline editor, binary in the upload-binary option.
668
+ const file = snapshot.binaryFile ??
669
+ new File([snapshot.binary ?? ''], snapshot.name, { type: snapshot.binaryType });
670
+ const saved = await this.deviceConfigurationService.openConfigurationDrawer(ConfigurationDetailComponent, {
671
+ version: snapshot.name,
672
+ deviceType: snapshot.deviceType,
673
+ description: snapshot.description,
674
+ binary: { file },
675
+ // No id — the drawer treats this as a new entry, but carries the
676
+ // configuration type so the user can review and edit before saving.
677
+ mo: { configurationType: snapshot.configurationType }
678
+ });
679
+ if (saved) {
380
680
  this.deviceConfigurationService.updateConfigurations(true);
381
- modal.close();
382
- }
383
- catch (ex) {
384
- // do nothing
385
681
  }
386
682
  }
387
683
  hasPermission() {
@@ -409,21 +705,20 @@ class ConfigurationPreviewComponent {
409
705
  this.deviceConfigurationService.updateConfigurations();
410
706
  }
411
707
  }
412
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationPreviewComponent, deps: [{ token: DeviceConfigurationService }, { token: i2.OperationRealtimeService }, { token: i1$1.BsModalService }, { token: i4.UserService }, { token: i2.AppStateService }, { token: i3.RepositoryService }, { token: i4.OperationService }, { token: i2.AlertService }], target: i0.ɵɵFactoryTarget.Component }); }
413
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: ConfigurationPreviewComponent, isStandalone: true, selector: "c8y-device-configuration-preview", inputs: { device: "device", configurationType: "configurationType", configSnapshot: "configSnapshot", canSaveSnapshot: "canSaveSnapshot", actionButtonText: "actionButtonText", actionButtonIcon: "actionButtonIcon", isLegacy: "isLegacy", operationToTrigger: "operationToTrigger" }, ngImport: i0, template: "<div class=\"content-flex-55 p-b-16\">\n <div class=\"col-7 p-t-4\">\n <p>\n <span class=\"text-label-small text-uppercase m-r-4\" translate>Configuration</span>\n <span *ngIf=\"configSnapshot?.name; else emptyText\">\n <strong>{{ configSnapshot.name }}</strong>\n </span>\n <ng-template #emptyText>---</ng-template>\n </p>\n <p>\n <span class=\"text-label-small text-uppercase m-r-4\" translate>Last updated</span>\n <small *ngIf=\"configSnapshot?.time; else emptyDate\">\n {{ configSnapshot.time | c8yDate }}\n </small>\n <ng-template #emptyDate>---</ng-template>\n </p>\n </div>\n <div class=\"col-5\">\n <button\n id=\"action-btn\"\n class=\"btn btn-default btn-sm pull-right\"\n type=\"button\"\n title=\"{{ actionButtonText | translate }}\"\n (click)=\"createDeviceOperation()\"\n [disabled]=\"isCreateOperationDisabled()\"\n *ngIf=\"canCallAction\"\n >\n <i [c8yIcon]=\"actionButtonIcon\"></i>\n {{ actionButtonText | translate }}\n </button>\n </div>\n</div>\n<div class=\"c8y-empty-state text-left\" *ngIf=\"!configSnapshot?.binary && showBinary()\">\n <h1 [c8yIcon]=\"'file-image-o'\"></h1>\n <p>\n <strong translate>No preview available.</strong>\n <br />\n <small *ngIf=\"configSnapshot?.binary !== ''; else emptyFile\" translate>\n The file is not available.\n </small>\n <ng-template #emptyFile>\n <small translate>The file is empty.</small>\n </ng-template>\n </p>\n</div>\n<div *ngIf=\"configSnapshot?.binary && showBinary()\" class=\"flex-grow d-flex d-col\">\n <c8y-source-code-preview\n [text]=\"configSnapshot.binary\"\n [isDisabled]=\"true\"\n class=\"d-contents\"\n ></c8y-source-code-preview>\n <div *ngIf=\"canSaveSnapshot\" class=\"p-t-16\">\n <button\n title=\"{{ 'Download' | translate }}\"\n type=\"button\"\n class=\"btn btn-primary btn-sm pull-right m-l-8\"\n (click)=\"download()\"\n >\n {{ 'Download' | translate }}\n </button>\n <button\n title=\"{{ 'Save to repository' | translate }}\"\n *ngIf=\"hasPermission()\"\n type=\"button\"\n class=\"btn btn-default btn-sm pull-right\"\n (click)=\"saveToRepository()\"\n >\n {{ 'Save to repository' | translate }}\n </button>\n </div>\n</div>\n<div *ngIf=\"showOperation()\">\n <c8y-operation-details [operation]=\"operation\"></c8y-operation-details>\n</div>\n", dependencies: [{ kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: SourceCodePreviewComponent, selector: "c8y-source-code-preview", inputs: ["isDisabled", "text"] }, { kind: "component", type: OperationDetailsComponent, selector: "c8y-operation-details", inputs: ["operation"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }] }); }
708
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationPreviewComponent, deps: [{ token: DeviceConfigurationService }, { token: i2.OperationRealtimeService }, { token: i3$1.UserService }, { token: i2.AppStateService }, { token: i3.RepositoryService }, { token: i3$1.OperationService }, { token: i2.AlertService }], target: i0.ɵɵFactoryTarget.Component }); }
709
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ConfigurationPreviewComponent, isStandalone: true, selector: "c8y-device-configuration-preview", inputs: { device: "device", configurationType: "configurationType", configSnapshot: "configSnapshot", canSaveSnapshot: "canSaveSnapshot", actionButtonText: "actionButtonText", actionButtonIcon: "actionButtonIcon", isLegacy: "isLegacy", operationToTrigger: "operationToTrigger", editableRepositoryMo: "editableRepositoryMo" }, outputs: { editInRepository: "editInRepository" }, ngImport: i0, template: "<div class=\"content-flex-55 p-b-16\">\n <div class=\"col-7 p-t-4\">\n <p>\n <span\n class=\"text-label-small text-uppercase m-r-4\"\n translate\n >Configuration</span\n >\n @if (configSnapshot?.name) {\n <span>\n <strong>{{ configSnapshot.name }}</strong>\n </span>\n } @else {\n ---\n }\n </p>\n <p>\n <span\n class=\"text-label-small text-uppercase m-r-4\"\n translate\n >Last updated</span\n >\n @if (configSnapshot?.time) {\n <small>\n {{ configSnapshot.time | c8yDate }}\n </small>\n } @else {\n ---\n }\n </p>\n </div>\n <div class=\"col-5 d-flex j-c-end gap-4\">\n @if (editableRepositoryMo) {\n <button\n class=\"btn btn-default btn-sm\"\n title=\"{{ 'Edit in repository' | translate }}\"\n type=\"button\"\n (click)=\"editInRepository.emit(editableRepositoryMo)\"\n >\n <i\n [c8yIcon]=\"'pencil'\"\n aria-hidden=\"true\"\n ></i>\n {{ 'Edit in repository' | translate }}\n </button>\n }\n @if (canCallAction) {\n <button\n class=\"btn btn-default btn-sm\"\n title=\"{{ actionButtonText | translate }}\"\n id=\"action-btn\"\n type=\"button\"\n (click)=\"createDeviceOperation()\"\n [disabled]=\"isCreateOperationDisabled()\"\n >\n <i [c8yIcon]=\"actionButtonIcon\"></i>\n {{ actionButtonText | translate }}\n </button>\n }\n </div>\n</div>\n@if (!configSnapshot?.binary && showBinary()) {\n <div class=\"c8y-empty-state text-left\">\n <h1 [c8yIcon]=\"'file-image-o'\"></h1>\n <p>\n <strong translate>No preview available.</strong>\n <br />\n @if (configSnapshot?.binary !== '') {\n <small translate> The file is not available. </small>\n } @else {\n <small translate>The file is empty.</small>\n }\n </p>\n </div>\n}\n@if (configSnapshot?.binary && showBinary()) {\n <div class=\"flex-grow d-flex d-col\">\n <c8y-source-code-preview\n class=\"d-contents\"\n [text]=\"configSnapshot.binary\"\n [binaryType]=\"configSnapshot.binaryType\"\n [isDisabled]=\"true\"\n ></c8y-source-code-preview>\n @if (canSaveSnapshot) {\n <div class=\"p-t-16\">\n <button\n class=\"btn btn-primary btn-sm pull-right m-l-8\"\n title=\"{{ 'Download' | translate }}\"\n type=\"button\"\n (click)=\"download()\"\n >\n {{ 'Download' | translate }}\n </button>\n @if (hasPermission()) {\n <button\n class=\"btn btn-default btn-sm pull-right\"\n title=\"{{ 'Save to repository' | translate }}\"\n type=\"button\"\n (click)=\"saveToRepository()\"\n >\n {{ 'Save to repository' | translate }}\n </button>\n }\n </div>\n }\n </div>\n}\n@if (showOperation()) {\n <div>\n <c8y-operation-details [operation]=\"operation\"></c8y-operation-details>\n </div>\n}\n", dependencies: [{ kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "component", type: SourceCodePreviewComponent, selector: "c8y-source-code-preview", inputs: ["isDisabled", "text", "binaryType"] }, { kind: "component", type: OperationDetailsComponent, selector: "c8y-operation-details", inputs: ["operation"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: DatePipe, name: "c8yDate" }] }); }
414
710
  }
415
711
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationPreviewComponent, decorators: [{
416
712
  type: Component,
417
713
  args: [{ selector: 'c8y-device-configuration-preview', imports: [
418
714
  C8yTranslateDirective,
419
- NgIf,
420
715
  IconDirective,
421
716
  SourceCodePreviewComponent,
422
717
  OperationDetailsComponent,
423
718
  C8yTranslatePipe,
424
719
  DatePipe
425
- ], template: "<div class=\"content-flex-55 p-b-16\">\n <div class=\"col-7 p-t-4\">\n <p>\n <span class=\"text-label-small text-uppercase m-r-4\" translate>Configuration</span>\n <span *ngIf=\"configSnapshot?.name; else emptyText\">\n <strong>{{ configSnapshot.name }}</strong>\n </span>\n <ng-template #emptyText>---</ng-template>\n </p>\n <p>\n <span class=\"text-label-small text-uppercase m-r-4\" translate>Last updated</span>\n <small *ngIf=\"configSnapshot?.time; else emptyDate\">\n {{ configSnapshot.time | c8yDate }}\n </small>\n <ng-template #emptyDate>---</ng-template>\n </p>\n </div>\n <div class=\"col-5\">\n <button\n id=\"action-btn\"\n class=\"btn btn-default btn-sm pull-right\"\n type=\"button\"\n title=\"{{ actionButtonText | translate }}\"\n (click)=\"createDeviceOperation()\"\n [disabled]=\"isCreateOperationDisabled()\"\n *ngIf=\"canCallAction\"\n >\n <i [c8yIcon]=\"actionButtonIcon\"></i>\n {{ actionButtonText | translate }}\n </button>\n </div>\n</div>\n<div class=\"c8y-empty-state text-left\" *ngIf=\"!configSnapshot?.binary && showBinary()\">\n <h1 [c8yIcon]=\"'file-image-o'\"></h1>\n <p>\n <strong translate>No preview available.</strong>\n <br />\n <small *ngIf=\"configSnapshot?.binary !== ''; else emptyFile\" translate>\n The file is not available.\n </small>\n <ng-template #emptyFile>\n <small translate>The file is empty.</small>\n </ng-template>\n </p>\n</div>\n<div *ngIf=\"configSnapshot?.binary && showBinary()\" class=\"flex-grow d-flex d-col\">\n <c8y-source-code-preview\n [text]=\"configSnapshot.binary\"\n [isDisabled]=\"true\"\n class=\"d-contents\"\n ></c8y-source-code-preview>\n <div *ngIf=\"canSaveSnapshot\" class=\"p-t-16\">\n <button\n title=\"{{ 'Download' | translate }}\"\n type=\"button\"\n class=\"btn btn-primary btn-sm pull-right m-l-8\"\n (click)=\"download()\"\n >\n {{ 'Download' | translate }}\n </button>\n <button\n title=\"{{ 'Save to repository' | translate }}\"\n *ngIf=\"hasPermission()\"\n type=\"button\"\n class=\"btn btn-default btn-sm pull-right\"\n (click)=\"saveToRepository()\"\n >\n {{ 'Save to repository' | translate }}\n </button>\n </div>\n</div>\n<div *ngIf=\"showOperation()\">\n <c8y-operation-details [operation]=\"operation\"></c8y-operation-details>\n</div>\n" }]
426
- }], ctorParameters: () => [{ type: DeviceConfigurationService }, { type: i2.OperationRealtimeService }, { type: i1$1.BsModalService }, { type: i4.UserService }, { type: i2.AppStateService }, { type: i3.RepositoryService }, { type: i4.OperationService }, { type: i2.AlertService }], propDecorators: { device: [{
720
+ ], template: "<div class=\"content-flex-55 p-b-16\">\n <div class=\"col-7 p-t-4\">\n <p>\n <span\n class=\"text-label-small text-uppercase m-r-4\"\n translate\n >Configuration</span\n >\n @if (configSnapshot?.name) {\n <span>\n <strong>{{ configSnapshot.name }}</strong>\n </span>\n } @else {\n ---\n }\n </p>\n <p>\n <span\n class=\"text-label-small text-uppercase m-r-4\"\n translate\n >Last updated</span\n >\n @if (configSnapshot?.time) {\n <small>\n {{ configSnapshot.time | c8yDate }}\n </small>\n } @else {\n ---\n }\n </p>\n </div>\n <div class=\"col-5 d-flex j-c-end gap-4\">\n @if (editableRepositoryMo) {\n <button\n class=\"btn btn-default btn-sm\"\n title=\"{{ 'Edit in repository' | translate }}\"\n type=\"button\"\n (click)=\"editInRepository.emit(editableRepositoryMo)\"\n >\n <i\n [c8yIcon]=\"'pencil'\"\n aria-hidden=\"true\"\n ></i>\n {{ 'Edit in repository' | translate }}\n </button>\n }\n @if (canCallAction) {\n <button\n class=\"btn btn-default btn-sm\"\n title=\"{{ actionButtonText | translate }}\"\n id=\"action-btn\"\n type=\"button\"\n (click)=\"createDeviceOperation()\"\n [disabled]=\"isCreateOperationDisabled()\"\n >\n <i [c8yIcon]=\"actionButtonIcon\"></i>\n {{ actionButtonText | translate }}\n </button>\n }\n </div>\n</div>\n@if (!configSnapshot?.binary && showBinary()) {\n <div class=\"c8y-empty-state text-left\">\n <h1 [c8yIcon]=\"'file-image-o'\"></h1>\n <p>\n <strong translate>No preview available.</strong>\n <br />\n @if (configSnapshot?.binary !== '') {\n <small translate> The file is not available. </small>\n } @else {\n <small translate>The file is empty.</small>\n }\n </p>\n </div>\n}\n@if (configSnapshot?.binary && showBinary()) {\n <div class=\"flex-grow d-flex d-col\">\n <c8y-source-code-preview\n class=\"d-contents\"\n [text]=\"configSnapshot.binary\"\n [binaryType]=\"configSnapshot.binaryType\"\n [isDisabled]=\"true\"\n ></c8y-source-code-preview>\n @if (canSaveSnapshot) {\n <div class=\"p-t-16\">\n <button\n class=\"btn btn-primary btn-sm pull-right m-l-8\"\n title=\"{{ 'Download' | translate }}\"\n type=\"button\"\n (click)=\"download()\"\n >\n {{ 'Download' | translate }}\n </button>\n @if (hasPermission()) {\n <button\n class=\"btn btn-default btn-sm pull-right\"\n title=\"{{ 'Save to repository' | translate }}\"\n type=\"button\"\n (click)=\"saveToRepository()\"\n >\n {{ 'Save to repository' | translate }}\n </button>\n }\n </div>\n }\n </div>\n}\n@if (showOperation()) {\n <div>\n <c8y-operation-details [operation]=\"operation\"></c8y-operation-details>\n </div>\n}\n" }]
721
+ }], ctorParameters: () => [{ type: DeviceConfigurationService }, { type: i2.OperationRealtimeService }, { type: i3$1.UserService }, { type: i2.AppStateService }, { type: i3.RepositoryService }, { type: i3$1.OperationService }, { type: i2.AlertService }], propDecorators: { device: [{
427
722
  type: Input
428
723
  }], configurationType: [{
429
724
  type: Input
@@ -439,6 +734,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
439
734
  type: Input
440
735
  }], operationToTrigger: [{
441
736
  type: Input
737
+ }], editableRepositoryMo: [{
738
+ type: Input
739
+ }], editInRepository: [{
740
+ type: Output
442
741
  }] } });
443
742
 
444
743
  class DeviceConfigurationListComponent {
@@ -479,10 +778,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
479
778
  }] } });
480
779
 
481
780
  class DeviceConfigurationComponent {
482
- constructor(route, deviceConfigurationService, realtime, repositoryService) {
781
+ constructor(route, deviceConfigurationService, repositoryService) {
483
782
  this.route = route;
484
783
  this.deviceConfigurationService = deviceConfigurationService;
485
- this.realtime = realtime;
486
784
  this.repositoryService = repositoryService;
487
785
  this.supportedConfigurations = [];
488
786
  this.showBinaryBasedConfig = false;
@@ -525,9 +823,11 @@ class DeviceConfigurationComponent {
525
823
  async onConfigTypeSelected(config) {
526
824
  this.configurationType = config.name;
527
825
  this.isLegacy = config.isLegacy;
826
+ this.selectedRepositoryMo = undefined;
528
827
  this.updateSnapshots();
529
828
  }
530
829
  async onRepositoryConfigSelected(config) {
830
+ this.selectedRepositoryMo = config;
531
831
  this.repositorySnapshot = {
532
832
  id: config.id,
533
833
  time: config.creationTime,
@@ -543,9 +843,10 @@ class DeviceConfigurationComponent {
543
843
  });
544
844
  if (binary) {
545
845
  this.repositorySnapshot.binary = await binary.text();
846
+ this.repositorySnapshot.binaryType = binary.type || undefined;
546
847
  }
547
848
  }
548
- catch (ex) {
849
+ catch {
549
850
  // do nothing
550
851
  }
551
852
  }
@@ -553,6 +854,7 @@ class DeviceConfigurationComponent {
553
854
  async updateSnapshots(repositorySnapsOnly) {
554
855
  this.reloading = true;
555
856
  this.repositorySnapshot = undefined;
857
+ this.selectedRepositoryMo = undefined;
556
858
  this.repositorySnapshots = await this.repositoryService.getSnapshotsFromRepository(this.device, this.configurationType);
557
859
  if (!repositorySnapsOnly) {
558
860
  this.configSnapshot = this.isLegacy
@@ -564,8 +866,27 @@ class DeviceConfigurationComponent {
564
866
  }
565
867
  this.reloading = false;
566
868
  }
567
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DeviceConfigurationComponent, deps: [{ token: i1.ActivatedRoute }, { token: DeviceConfigurationService }, { token: i4.Realtime }, { token: i3.RepositoryService }], target: i0.ɵɵFactoryTarget.Component }); }
568
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: DeviceConfigurationComponent, isStandalone: true, selector: "c8y-device-configuration", viewQueries: [{ propertyName: "textBasedConfigurationComponent", first: true, predicate: TextBasedConfigurationComponent, descendants: true }], ngImport: i0, template: "<c8y-action-bar-item [placement]=\"'right'\">\n <button class=\"btn btn-link\" title=\"{{ 'Reload' | translate }}\" (click)=\"updateSnapshots()\">\n <i c8yIcon=\"refresh\" [ngClass]=\"{ 'icon-spin': reloading }\"></i>\n {{ 'Reload' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<div class=\"card content-fullpage card-has-tabs\">\n <tabset>\n <div class=\"card-header separator\" *ngIf=\"showBinaryBasedConfig && !showTextBasedConfig\">\n <div class=\"card-title\">{{ 'Configurations' | translate }}</div>\n </div>\n <div class=\"card-header separator\" *ngIf=\"!showBinaryBasedConfig && showTextBasedConfig\">\n <div class=\"card-title\">{{ 'Text-based configuration' | translate }}</div>\n </div>\n <tab heading=\"{{ 'Configurations' | translate }}\" *ngIf=\"showBinaryBasedConfig\">\n <div class=\"card--grid card grid__col--4-8--md grid__row--6-6--md m-b-0\">\n <!-- DEVICE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5 class=\"legend form-block\">\n <span translate>Device-supported configurations</span>\n </h5>\n </div>\n <c8y-device-configuration-list\n [items]=\"supportedConfigurations\"\n [itemIcon]=\"'gears'\"\n (configSelected)=\"onConfigTypeSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block d-flex d-col flex-grow\">\n <h5 class=\"legend form-block\"><span translate>Preview</span></h5>\n\n <!-- EMPTY STATE -->\n\n <c8y-ui-empty-state\n *ngIf=\"!configurationType\"\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n\n <!-- PREVIEW AVAILABLE STATE -->\n <c8y-device-configuration-preview\n *ngIf=\"configurationType\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"configSnapshot\"\n [canSaveSnapshot]=\"true\"\n [operationToTrigger]=\"'c8y_UploadConfigFile'\"\n [actionButtonText]=\"'Get snapshot from device' | translate\"\n [actionButtonIcon]=\"'download'\"\n [isLegacy]=\"isLegacy\"\n class=\"d-flex d-col flex-grow\"\n ></c8y-device-configuration-preview>\n </div>\n </div>\n\n <!-- AVAILABLE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5 class=\"legend form-block\" translate>Available supported configurations</h5>\n </div>\n\n <!-- EMPTY STATE -->\n\n <c8y-ui-empty-state\n *ngIf=\"!configurationType\"\n [icon]=\"'gears'\"\n [title]=\"'No selection.' | translate\"\n [subtitle]=\"\n 'Select a configuration from the device-supported configuration list.' | translate\n \"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n\n <div class=\"p-r-16\" *ngIf=\"configurationType\">\n <c8y-device-configuration-list\n [items]=\"repositorySnapshots\"\n [itemIcon]=\"'file-text'\"\n [emptyState]=\"repositorySnapshotsEmptyState\"\n [isFilterEnabled]=\"true\"\n (configSelected)=\"onRepositoryConfigSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n </div>\n\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block flex-grow d-flex d-col\">\n <h5 class=\"legend form-block\" translate>Preview</h5>\n\n <!-- EMPTY STATE -->\n <c8y-ui-empty-state\n *ngIf=\"!repositorySnapshot\"\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n\n <!-- CONFIGURATION SELECTED STATE -->\n <c8y-device-configuration-preview\n *ngIf=\"repositorySnapshot\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"repositorySnapshot\"\n [operationToTrigger]=\"'c8y_DownloadConfigFile'\"\n [actionButtonText]=\"'Send configuration to device' | translate\"\n [actionButtonIcon]=\"'upload'\"\n [isLegacy]=\"isLegacy\"\n class=\"d-flex d-col flex-grow\"\n ></c8y-device-configuration-preview>\n </div>\n </div>\n </div>\n </tab>\n <tab heading=\"{{ 'Text-based configuration' | translate }}\" *ngIf=\"showTextBasedConfig\">\n <c8y-text-based-configuration></c8y-text-based-configuration>\n </tab>\n </tabset>\n</div>\n", dependencies: [{ kind: "component", type: ActionBarItemComponent, selector: "c8y-action-bar-item", inputs: ["placement", "priority", "itemClass", "injector", "groupId", "inGroupPriority"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: TabsetAriaDirective, selector: "tabset" }, { kind: "component", type: TabsetComponent, selector: "tabset", inputs: ["vertical", "justified", "type"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: TabDirective, selector: "tab, [tab]", inputs: ["heading", "id", "disabled", "removable", "tabOrder", "customClass", "active"], outputs: ["selectTab", "deselect", "removed"], exportAs: ["tab"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: DeviceConfigurationListComponent, selector: "c8y-device-configuration-list", inputs: ["items", "itemIcon", "emptyState", "isFilterEnabled"], outputs: ["configSelected"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "component", type: ConfigurationPreviewComponent, selector: "c8y-device-configuration-preview", inputs: ["device", "configurationType", "configSnapshot", "canSaveSnapshot", "actionButtonText", "actionButtonIcon", "isLegacy", "operationToTrigger"] }, { kind: "component", type: TextBasedConfigurationComponent, selector: "c8y-text-based-configuration" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
869
+ async openEditInRepository(mo) {
870
+ let fileBinary = null;
871
+ try {
872
+ fileBinary = await this.repositoryService.getBinaryFile(mo.url, { allowExternal: false });
873
+ }
874
+ catch {
875
+ // Binary unavailable — drawer still opens; editor will be empty
876
+ }
877
+ const saved = await this.deviceConfigurationService.openConfigurationDrawer(ConfigurationDetailComponent, {
878
+ version: mo.name,
879
+ deviceType: mo.deviceType,
880
+ description: mo.description,
881
+ binary: { file: fileBinary, url: mo.url },
882
+ mo
883
+ });
884
+ if (saved) {
885
+ this.updateSnapshots(true);
886
+ }
887
+ }
888
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DeviceConfigurationComponent, deps: [{ token: i1.ActivatedRoute }, { token: DeviceConfigurationService }, { token: i3.RepositoryService }], target: i0.ɵɵFactoryTarget.Component }); }
889
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: DeviceConfigurationComponent, isStandalone: true, selector: "c8y-device-configuration", viewQueries: [{ propertyName: "textBasedConfigurationComponent", first: true, predicate: TextBasedConfigurationComponent, descendants: true }], ngImport: i0, template: "<c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Reload' | translate }}\"\n (click)=\"updateSnapshots()\"\n >\n <i\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': reloading }\"\n ></i>\n {{ 'Reload' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<div class=\"card content-fullpage card-has-tabs\">\n <tabset>\n @if (showBinaryBasedConfig && !showTextBasedConfig) {\n <div class=\"card-header separator\">\n <div class=\"card-title\">{{ 'Configurations' | translate }}</div>\n </div>\n }\n @if (!showBinaryBasedConfig && showTextBasedConfig) {\n <div class=\"card-header separator\">\n <div class=\"card-title\">{{ 'Text-based configuration' | translate }}</div>\n </div>\n }\n @if (showBinaryBasedConfig) {\n <tab heading=\"{{ 'Configurations' | translate }}\">\n <div class=\"card--grid card grid__col--4-8--md grid__row--6-6--md m-b-0\">\n <!-- DEVICE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5 class=\"legend form-block\">\n <span translate>Device-supported configurations</span>\n </h5>\n </div>\n <c8y-device-configuration-list\n [itemIcon]=\"'gears'\"\n [items]=\"supportedConfigurations\"\n (configSelected)=\"onConfigTypeSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block d-flex d-col flex-grow\">\n <h5 class=\"legend form-block\"><span translate>Preview</span></h5>\n <!-- EMPTY STATE -->\n @if (!configurationType) {\n <c8y-ui-empty-state\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n <!-- PREVIEW AVAILABLE STATE -->\n @if (configurationType) {\n <c8y-device-configuration-preview\n class=\"d-flex d-col flex-grow\"\n [actionButtonIcon]=\"'download'\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"configSnapshot\"\n [canSaveSnapshot]=\"true\"\n [operationToTrigger]=\"'c8y_UploadConfigFile'\"\n [actionButtonText]=\"'Get snapshot from device' | translate\"\n [isLegacy]=\"isLegacy\"\n ></c8y-device-configuration-preview>\n }\n </div>\n </div>\n <!-- AVAILABLE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5\n class=\"legend form-block\"\n translate\n >\n Available supported configurations\n </h5>\n </div>\n <!-- EMPTY STATE -->\n @if (!configurationType) {\n <c8y-ui-empty-state\n [icon]=\"'gears'\"\n [title]=\"'No selection.' | translate\"\n [subtitle]=\"\n 'Select a configuration from the device-supported configuration list.' | translate\n \"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n @if (configurationType) {\n <div class=\"p-r-16\">\n <c8y-device-configuration-list\n [itemIcon]=\"'file-text'\"\n [items]=\"repositorySnapshots\"\n [emptyState]=\"repositorySnapshotsEmptyState\"\n [isFilterEnabled]=\"true\"\n (configSelected)=\"onRepositoryConfigSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n }\n </div>\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block flex-grow d-flex d-col\">\n <h5\n class=\"legend form-block\"\n translate\n >\n Preview\n </h5>\n <!-- EMPTY STATE -->\n @if (!repositorySnapshot) {\n <c8y-ui-empty-state\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n <!-- CONFIGURATION SELECTED STATE -->\n @if (repositorySnapshot) {\n <c8y-device-configuration-preview\n class=\"d-flex d-col flex-grow\"\n [actionButtonIcon]=\"'upload'\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"repositorySnapshot\"\n [operationToTrigger]=\"'c8y_DownloadConfigFile'\"\n [actionButtonText]=\"'Send configuration to device' | translate\"\n [isLegacy]=\"isLegacy\"\n [editableRepositoryMo]=\"selectedRepositoryMo\"\n (editInRepository)=\"openEditInRepository($event)\"\n ></c8y-device-configuration-preview>\n }\n </div>\n </div>\n </div>\n </tab>\n }\n @if (showTextBasedConfig) {\n <tab heading=\"{{ 'Text-based configuration' | translate }}\">\n <c8y-text-based-configuration></c8y-text-based-configuration>\n </tab>\n }\n </tabset>\n</div>\n", dependencies: [{ kind: "component", type: ActionBarItemComponent, selector: "c8y-action-bar-item", inputs: ["placement", "priority", "itemClass", "injector", "groupId", "inGroupPriority"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: TabsetAriaDirective, selector: "tabset" }, { kind: "component", type: TabsetComponent, selector: "tabset", inputs: ["vertical", "justified", "type"] }, { kind: "directive", type: TabDirective, selector: "tab, [tab]", inputs: ["heading", "id", "disabled", "removable", "tabOrder", "customClass", "active"], outputs: ["selectTab", "deselect", "removed"], exportAs: ["tab"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: DeviceConfigurationListComponent, selector: "c8y-device-configuration-list", inputs: ["items", "itemIcon", "emptyState", "isFilterEnabled"], outputs: ["configSelected"] }, { kind: "component", type: EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "component", type: ConfigurationPreviewComponent, selector: "c8y-device-configuration-preview", inputs: ["device", "configurationType", "configSnapshot", "canSaveSnapshot", "actionButtonText", "actionButtonIcon", "isLegacy", "operationToTrigger", "editableRepositoryMo"], outputs: ["editInRepository"] }, { kind: "component", type: TextBasedConfigurationComponent, selector: "c8y-text-based-configuration" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
569
890
  }
570
891
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: DeviceConfigurationComponent, decorators: [{
571
892
  type: Component,
@@ -575,7 +896,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
575
896
  NgClass,
576
897
  TabsetAriaDirective,
577
898
  TabsetComponent,
578
- NgIf,
579
899
  TabDirective,
580
900
  C8yTranslateDirective,
581
901
  DeviceConfigurationListComponent,
@@ -583,12 +903,64 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
583
903
  ConfigurationPreviewComponent,
584
904
  TextBasedConfigurationComponent,
585
905
  C8yTranslatePipe
586
- ], template: "<c8y-action-bar-item [placement]=\"'right'\">\n <button class=\"btn btn-link\" title=\"{{ 'Reload' | translate }}\" (click)=\"updateSnapshots()\">\n <i c8yIcon=\"refresh\" [ngClass]=\"{ 'icon-spin': reloading }\"></i>\n {{ 'Reload' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<div class=\"card content-fullpage card-has-tabs\">\n <tabset>\n <div class=\"card-header separator\" *ngIf=\"showBinaryBasedConfig && !showTextBasedConfig\">\n <div class=\"card-title\">{{ 'Configurations' | translate }}</div>\n </div>\n <div class=\"card-header separator\" *ngIf=\"!showBinaryBasedConfig && showTextBasedConfig\">\n <div class=\"card-title\">{{ 'Text-based configuration' | translate }}</div>\n </div>\n <tab heading=\"{{ 'Configurations' | translate }}\" *ngIf=\"showBinaryBasedConfig\">\n <div class=\"card--grid card grid__col--4-8--md grid__row--6-6--md m-b-0\">\n <!-- DEVICE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5 class=\"legend form-block\">\n <span translate>Device-supported configurations</span>\n </h5>\n </div>\n <c8y-device-configuration-list\n [items]=\"supportedConfigurations\"\n [itemIcon]=\"'gears'\"\n (configSelected)=\"onConfigTypeSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block d-flex d-col flex-grow\">\n <h5 class=\"legend form-block\"><span translate>Preview</span></h5>\n\n <!-- EMPTY STATE -->\n\n <c8y-ui-empty-state\n *ngIf=\"!configurationType\"\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n\n <!-- PREVIEW AVAILABLE STATE -->\n <c8y-device-configuration-preview\n *ngIf=\"configurationType\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"configSnapshot\"\n [canSaveSnapshot]=\"true\"\n [operationToTrigger]=\"'c8y_UploadConfigFile'\"\n [actionButtonText]=\"'Get snapshot from device' | translate\"\n [actionButtonIcon]=\"'download'\"\n [isLegacy]=\"isLegacy\"\n class=\"d-flex d-col flex-grow\"\n ></c8y-device-configuration-preview>\n </div>\n </div>\n\n <!-- AVAILABLE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5 class=\"legend form-block\" translate>Available supported configurations</h5>\n </div>\n\n <!-- EMPTY STATE -->\n\n <c8y-ui-empty-state\n *ngIf=\"!configurationType\"\n [icon]=\"'gears'\"\n [title]=\"'No selection.' | translate\"\n [subtitle]=\"\n 'Select a configuration from the device-supported configuration list.' | translate\n \"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n\n <div class=\"p-r-16\" *ngIf=\"configurationType\">\n <c8y-device-configuration-list\n [items]=\"repositorySnapshots\"\n [itemIcon]=\"'file-text'\"\n [emptyState]=\"repositorySnapshotsEmptyState\"\n [isFilterEnabled]=\"true\"\n (configSelected)=\"onRepositoryConfigSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n </div>\n\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block flex-grow d-flex d-col\">\n <h5 class=\"legend form-block\" translate>Preview</h5>\n\n <!-- EMPTY STATE -->\n <c8y-ui-empty-state\n *ngIf=\"!repositorySnapshot\"\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n\n <!-- CONFIGURATION SELECTED STATE -->\n <c8y-device-configuration-preview\n *ngIf=\"repositorySnapshot\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"repositorySnapshot\"\n [operationToTrigger]=\"'c8y_DownloadConfigFile'\"\n [actionButtonText]=\"'Send configuration to device' | translate\"\n [actionButtonIcon]=\"'upload'\"\n [isLegacy]=\"isLegacy\"\n class=\"d-flex d-col flex-grow\"\n ></c8y-device-configuration-preview>\n </div>\n </div>\n </div>\n </tab>\n <tab heading=\"{{ 'Text-based configuration' | translate }}\" *ngIf=\"showTextBasedConfig\">\n <c8y-text-based-configuration></c8y-text-based-configuration>\n </tab>\n </tabset>\n</div>\n" }]
587
- }], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: DeviceConfigurationService }, { type: i4.Realtime }, { type: i3.RepositoryService }], propDecorators: { textBasedConfigurationComponent: [{
906
+ ], template: "<c8y-action-bar-item [placement]=\"'right'\">\n <button\n class=\"btn btn-link\"\n title=\"{{ 'Reload' | translate }}\"\n (click)=\"updateSnapshots()\"\n >\n <i\n c8yIcon=\"refresh\"\n [ngClass]=\"{ 'icon-spin': reloading }\"\n ></i>\n {{ 'Reload' | translate }}\n </button>\n</c8y-action-bar-item>\n\n<div class=\"card content-fullpage card-has-tabs\">\n <tabset>\n @if (showBinaryBasedConfig && !showTextBasedConfig) {\n <div class=\"card-header separator\">\n <div class=\"card-title\">{{ 'Configurations' | translate }}</div>\n </div>\n }\n @if (!showBinaryBasedConfig && showTextBasedConfig) {\n <div class=\"card-header separator\">\n <div class=\"card-title\">{{ 'Text-based configuration' | translate }}</div>\n </div>\n }\n @if (showBinaryBasedConfig) {\n <tab heading=\"{{ 'Configurations' | translate }}\">\n <div class=\"card--grid card grid__col--4-8--md grid__row--6-6--md m-b-0\">\n <!-- DEVICE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5 class=\"legend form-block\">\n <span translate>Device-supported configurations</span>\n </h5>\n </div>\n <c8y-device-configuration-list\n [itemIcon]=\"'gears'\"\n [items]=\"supportedConfigurations\"\n (configSelected)=\"onConfigTypeSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block d-flex d-col flex-grow\">\n <h5 class=\"legend form-block\"><span translate>Preview</span></h5>\n <!-- EMPTY STATE -->\n @if (!configurationType) {\n <c8y-ui-empty-state\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n <!-- PREVIEW AVAILABLE STATE -->\n @if (configurationType) {\n <c8y-device-configuration-preview\n class=\"d-flex d-col flex-grow\"\n [actionButtonIcon]=\"'download'\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"configSnapshot\"\n [canSaveSnapshot]=\"true\"\n [operationToTrigger]=\"'c8y_UploadConfigFile'\"\n [actionButtonText]=\"'Get snapshot from device' | translate\"\n [isLegacy]=\"isLegacy\"\n ></c8y-device-configuration-preview>\n }\n </div>\n </div>\n <!-- AVAILABLE SUPPORTED CONFIGURATIONS -->\n <div class=\"card--grid__inner-scroll bg-level-1\">\n <div class=\"p-l-16 p-r-16\">\n <h5\n class=\"legend form-block\"\n translate\n >\n Available supported configurations\n </h5>\n </div>\n <!-- EMPTY STATE -->\n @if (!configurationType) {\n <c8y-ui-empty-state\n [icon]=\"'gears'\"\n [title]=\"'No selection.' | translate\"\n [subtitle]=\"\n 'Select a configuration from the device-supported configuration list.' | translate\n \"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n @if (configurationType) {\n <div class=\"p-r-16\">\n <c8y-device-configuration-list\n [itemIcon]=\"'file-text'\"\n [items]=\"repositorySnapshots\"\n [emptyState]=\"repositorySnapshotsEmptyState\"\n [isFilterEnabled]=\"true\"\n (configSelected)=\"onRepositoryConfigSelected($event)\"\n ></c8y-device-configuration-list>\n </div>\n }\n </div>\n <!-- CONFIGURATION PREVIEW -->\n <div class=\"card--grid__inner-scroll d-flex d-col flex-grow\">\n <div class=\"card-block flex-grow d-flex d-col\">\n <h5\n class=\"legend form-block\"\n translate\n >\n Preview\n </h5>\n <!-- EMPTY STATE -->\n @if (!repositorySnapshot) {\n <c8y-ui-empty-state\n [icon]=\"'file-text'\"\n [title]=\"'No configuration selected.' | translate\"\n [subtitle]=\"'Select a configuration to preview.' | translate\"\n [horizontal]=\"true\"\n ></c8y-ui-empty-state>\n }\n <!-- CONFIGURATION SELECTED STATE -->\n @if (repositorySnapshot) {\n <c8y-device-configuration-preview\n class=\"d-flex d-col flex-grow\"\n [actionButtonIcon]=\"'upload'\"\n [device]=\"device\"\n [configurationType]=\"configurationType\"\n [configSnapshot]=\"repositorySnapshot\"\n [operationToTrigger]=\"'c8y_DownloadConfigFile'\"\n [actionButtonText]=\"'Send configuration to device' | translate\"\n [isLegacy]=\"isLegacy\"\n [editableRepositoryMo]=\"selectedRepositoryMo\"\n (editInRepository)=\"openEditInRepository($event)\"\n ></c8y-device-configuration-preview>\n }\n </div>\n </div>\n </div>\n </tab>\n }\n @if (showTextBasedConfig) {\n <tab heading=\"{{ 'Text-based configuration' | translate }}\">\n <c8y-text-based-configuration></c8y-text-based-configuration>\n </tab>\n }\n </tabset>\n</div>\n" }]
907
+ }], ctorParameters: () => [{ type: i1.ActivatedRoute }, { type: DeviceConfigurationService }, { type: i3.RepositoryService }], propDecorators: { textBasedConfigurationComponent: [{
588
908
  type: ViewChild,
589
909
  args: [TextBasedConfigurationComponent]
590
910
  }] } });
591
911
 
912
+ class SaveToRepositoryComponent {
913
+ constructor(modal, alertService, repositoryService) {
914
+ this.modal = modal;
915
+ this.alertService = alertService;
916
+ this.repositoryService = repositoryService;
917
+ this.result = new Promise((resolve, reject) => {
918
+ this._save = resolve;
919
+ this._cancel = reject;
920
+ });
921
+ }
922
+ async save() {
923
+ {
924
+ try {
925
+ const configSnapshotData = {
926
+ selected: {
927
+ configurationType: this.configSnapshot.configurationType
928
+ },
929
+ version: this.configSnapshot.name,
930
+ deviceType: this.configSnapshot.deviceType,
931
+ description: this.configSnapshot.description,
932
+ binary: {
933
+ file: new File([this.configSnapshot.binary], this.configSnapshot.name)
934
+ }
935
+ };
936
+ await this.repositoryService.create(configSnapshotData, RepositoryType.CONFIGURATION);
937
+ this.alertService.success(gettext('Configuration saved.'));
938
+ this._save();
939
+ }
940
+ catch (ex) {
941
+ this.alertService.addServerFailure(ex);
942
+ }
943
+ }
944
+ }
945
+ close() {
946
+ this._cancel();
947
+ this.modal.hide();
948
+ }
949
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SaveToRepositoryComponent, deps: [{ token: i1$2.BsModalRef }, { token: i2.AlertService }, { token: i3.RepositoryService }], target: i0.ɵɵFactoryTarget.Component }); }
950
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: SaveToRepositoryComponent, isStandalone: true, selector: "c8y-save-config-to-configuration-repository", ngImport: i0, template: "<div class=\"modal-header dialog-header\">\n <i c8yIcon=\"gears\"></i>\n <h4 id=\"modal-title\" translate>\n Save configuration\n </h4>\n</div>\n<div class=\"modal-body\" id=\"modal-body\">\n <form #saveConfigurationSnapshot=\"ngForm\" class=\"p-t-24\">\n <c8y-form-group>\n <label translate for=\"name\">Name</label>\n <input\n id=\"name\"\n type=\"text\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"name\"\n [(ngModel)]=\"configSnapshot.name\"\n required\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"deviceType\">Device type</label>\n <input\n id=\"deviceType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"deviceType\"\n [(ngModel)]=\"configSnapshot.deviceType\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"description\">Description</label>\n <input\n type=\"text\"\n id=\"description\"\n class=\"form-control\"\n maxlength=\"254\"\n autocomplete=\"off\"\n name=\"description\"\n [(ngModel)]=\"configSnapshot.description\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"configurationType\">Configuration type</label>\n <input\n id=\"configurationType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"configurationType\"\n [(ngModel)]=\"configSnapshot.configurationType\"\n />\n </c8y-form-group>\n </form>\n</div>\n<div class=\"modal-footer\">\n <button title=\"{{ 'Cancel' | translate }}\" class=\"btn btn-default\" (click)=\"close()\" translate>\n Cancel\n </button>\n\n <button\n title=\"{{ 'Save configuration to repository' | translate }}\"\n class=\"btn btn-primary\"\n (click)=\"save()\"\n [disabled]=\"saveConfigurationSnapshot.form.invalid\"\n translate\n >\n Save\n </button>\n</div>\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { 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.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); }
951
+ }
952
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SaveToRepositoryComponent, decorators: [{
953
+ type: Component,
954
+ args: [{ selector: 'c8y-save-config-to-configuration-repository', imports: [
955
+ IconDirective,
956
+ C8yTranslateDirective,
957
+ FormsModule,
958
+ FormGroupComponent,
959
+ RequiredInputPlaceholderDirective,
960
+ C8yTranslatePipe
961
+ ], template: "<div class=\"modal-header dialog-header\">\n <i c8yIcon=\"gears\"></i>\n <h4 id=\"modal-title\" translate>\n Save configuration\n </h4>\n</div>\n<div class=\"modal-body\" id=\"modal-body\">\n <form #saveConfigurationSnapshot=\"ngForm\" class=\"p-t-24\">\n <c8y-form-group>\n <label translate for=\"name\">Name</label>\n <input\n id=\"name\"\n type=\"text\"\n class=\"form-control\"\n autocomplete=\"off\"\n name=\"name\"\n [(ngModel)]=\"configSnapshot.name\"\n required\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"deviceType\">Device type</label>\n <input\n id=\"deviceType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"deviceType\"\n [(ngModel)]=\"configSnapshot.deviceType\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"description\">Description</label>\n <input\n type=\"text\"\n id=\"description\"\n class=\"form-control\"\n maxlength=\"254\"\n autocomplete=\"off\"\n name=\"description\"\n [(ngModel)]=\"configSnapshot.description\"\n />\n </c8y-form-group>\n <c8y-form-group>\n <label translate for=\"configurationType\">Configuration type</label>\n <input\n id=\"configurationType\"\n class=\"form-control\"\n rows=\"6\"\n name=\"configurationType\"\n [(ngModel)]=\"configSnapshot.configurationType\"\n />\n </c8y-form-group>\n </form>\n</div>\n<div class=\"modal-footer\">\n <button title=\"{{ 'Cancel' | translate }}\" class=\"btn btn-default\" (click)=\"close()\" translate>\n Cancel\n </button>\n\n <button\n title=\"{{ 'Save configuration to repository' | translate }}\"\n class=\"btn btn-primary\"\n (click)=\"save()\"\n [disabled]=\"saveConfigurationSnapshot.form.invalid\"\n translate\n >\n Save\n </button>\n</div>\n" }]
962
+ }], ctorParameters: () => [{ type: i1$2.BsModalRef }, { type: i2.AlertService }, { type: i3.RepositoryService }] });
963
+
592
964
  class ConfigurationRepositoryDeviceTabModule {
593
965
  static forRoot() {
594
966
  return {
@@ -609,7 +981,7 @@ class ConfigurationRepositoryDeviceTabModule {
609
981
  }
610
982
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationRepositoryDeviceTabModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
611
983
  static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationRepositoryDeviceTabModule, imports: [SharedRepositoryModule,
612
- OperationDetailsModule, i1$2.TabsModule, DeviceConfigurationComponent,
984
+ OperationDetailsModule, i1$3.TabsModule, DeviceConfigurationComponent,
613
985
  DeviceConfigurationListComponent,
614
986
  ConfigurationPreviewComponent,
615
987
  ConfigurationFilterPipe,
@@ -623,6 +995,7 @@ class ConfigurationRepositoryDeviceTabModule {
623
995
  DeviceConfigurationListComponent,
624
996
  ConfigurationPreviewComponent,
625
997
  SaveToRepositoryComponent,
998
+ SourceCodePreviewComponent,
626
999
  TextBasedConfigurationComponent] }); }
627
1000
  }
628
1001
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationRepositoryDeviceTabModule, decorators: [{
@@ -644,303 +1017,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
644
1017
  }]
645
1018
  }] });
646
1019
 
647
- const EXT_LANGUAGE_MAP = {
648
- json: 'json',
649
- yaml: 'yaml',
650
- yml: 'yaml',
651
- xml: 'xml',
652
- html: 'html',
653
- css: 'css',
654
- ts: 'typescript',
655
- js: 'javascript',
656
- md: 'markdown',
657
- toml: 'ini',
658
- properties: 'ini',
659
- ini: 'ini',
660
- conf: 'ini',
661
- cfg: 'ini',
662
- sh: 'shell'
663
- };
664
- function detectLanguage(filename) {
665
- const ext = (filename.split('.').pop() ?? '').toLowerCase();
666
- return EXT_LANGUAGE_MAP[ext] ?? 'plaintext';
667
- }
668
- const EXT_MIME_MAP = {
669
- json: 'application/json',
670
- xml: 'application/xml',
671
- yaml: 'application/yaml',
672
- yml: 'application/yaml',
673
- js: 'application/javascript',
674
- ts: 'application/typescript',
675
- html: 'text/html',
676
- css: 'text/css',
677
- md: 'text/markdown',
678
- sh: 'application/x-sh',
679
- toml: 'application/toml'
680
- };
681
- function mimeForFilename(filename) {
682
- const ext = (filename.split('.').pop() ?? '').toLowerCase();
683
- return EXT_MIME_MAP[ext] ?? 'text/plain';
684
- }
685
- /** Sentinel value meaning "derive language from filename". */
686
- const AUTO_LANGUAGE = 'auto';
687
- const EDITOR_LANGUAGES = [
688
- { value: AUTO_LANGUAGE, label: gettext('Auto (from filename)') },
689
- { value: 'json', label: gettext('JSON') },
690
- { value: 'javascript', label: gettext('JavaScript') },
691
- { value: 'typescript', label: gettext('TypeScript') },
692
- { value: 'css', label: gettext('CSS') },
693
- { value: 'html', label: gettext('HTML') },
694
- { value: 'yaml', label: gettext('YAML') },
695
- { value: 'markdown', label: gettext('Markdown') },
696
- { value: 'xml', label: gettext('XML') },
697
- { value: 'ini', label: gettext('INI / TOML / Properties') },
698
- { value: 'shell', label: gettext('Shell') },
699
- { value: 'plaintext', label: gettext('Plain text') }
700
- ];
701
- class ConfigurationDetailComponent {
702
- constructor() {
703
- this.repositoryService = inject(RepositoryService);
704
- this.bottomDrawerRef = inject(BottomDrawerRef);
705
- this.alert = inject(AlertService);
706
- this.inventoryBinaryService = inject(InventoryBinaryService);
707
- this.cdr = inject(ChangeDetectorRef);
708
- this.binary = { file: undefined, url: undefined };
709
- this.mo = {};
710
- this.form = new FormGroup({
711
- version: new FormControl('', [
712
- Validators.required,
713
- Validators.maxLength(254),
714
- Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
715
- ]),
716
- deviceType: new FormControl('', [
717
- Validators.maxLength(254),
718
- Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
719
- ]),
720
- description: new FormControl('', [
721
- Validators.maxLength(254),
722
- Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
723
- ]),
724
- uploadChoice: new FormControl('uploadBinary'),
725
- editorFilename: new FormControl('', [
726
- Validators.pattern(ValidationPattern.rules.noWhiteSpaceOnly.pattern)
727
- ]),
728
- editorLanguage: new FormControl(EDITOR_LANGUAGES[0]),
729
- editorContent: new FormControl('')
730
- });
731
- this.configurationTypeMO = {};
732
- this.saving = signal(false, ...(ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []));
733
- this.pattern = signal('', ...(ngDevMode ? [{ debugName: "pattern" }] : /* istanbul ignore next */ []));
734
- this.isExternalUrl = signal(false, ...(ngDevMode ? [{ debugName: "isExternalUrl" }] : /* istanbul ignore next */ []));
735
- this.ValidationPattern = ValidationPattern;
736
- this.textForConfigurationUrlPopover = gettext(`Path for binaries can vary depending on device agent implementation, for example:
737
- /configuration/binaries/configuration1.bin
738
- https://configuration/binary/123
739
- ftp://configuration/binary/123.tar.gz
740
- Configurations with external URLs only work with the configuration typed devices (file-based configuration), not with devices with a legacy configuration.
741
- `);
742
- this.result = new Promise((resolve, reject) => {
743
- this._save = resolve;
744
- this._cancel = reject;
745
- });
746
- this.settled = false;
747
- this.editorFilenameValue = toSignal(this.form.controls.editorFilename.valueChanges, {
748
- initialValue: ''
749
- });
750
- this.editorLanguageValue = toSignal(this.form.controls.editorLanguage.valueChanges, {
751
- initialValue: EDITOR_LANGUAGES[0]
752
- });
753
- /** User's manual pick (when not "auto") wins over auto-detection from filename. */
754
- this.editorLanguage = computed(() => {
755
- const picked = this.editorLanguageValue()?.value;
756
- return !picked || picked === AUTO_LANGUAGE
757
- ? detectLanguage(this.editorFilenameValue() ?? '')
758
- : picked;
759
- }, ...(ngDevMode ? [{ debugName: "editorLanguage" }] : /* istanbul ignore next */ []));
760
- this.editorLanguages = EDITOR_LANGUAGES;
761
- this.submitButtonTitle = computed(() => this.mo?.id ? gettext('Update configuration') : gettext('Add configuration'), ...(ngDevMode ? [{ debugName: "submitButtonTitle" }] : /* istanbul ignore next */ []));
762
- this.form.controls.uploadChoice.valueChanges.pipe(takeUntilDestroyed()).subscribe(choice => {
763
- const ctrl = this.form.controls.editorFilename;
764
- if (choice === 'editInline') {
765
- ctrl.addValidators(Validators.required);
766
- }
767
- else {
768
- ctrl.removeValidators(Validators.required);
769
- }
770
- ctrl.updateValueAndValidity();
771
- });
772
- // Drawer can be closed externally (Escape, navigation, programmatic close).
773
- // Make sure the result promise is always settled, otherwise callers awaiting it hang.
774
- this.bottomDrawerRef.onClosed$.pipe(takeUntilDestroyed()).subscribe(() => {
775
- if (!this.settled) {
776
- this.settled = true;
777
- this._cancel('Drawer closed');
778
- }
779
- });
780
- }
781
- async ngOnInit() {
782
- this.configs = await this.repositoryService.listRepositoryEntries(RepositoryType.CONFIGURATION);
783
- this.setPipe('');
784
- if (this.mo?.id) {
785
- this.configurationTypeMO = this.mo;
786
- const hasExternalUrl = !!this.binary?.url && !this.inventoryBinaryService.getIdFromUrl(this.binary.url);
787
- if (hasExternalUrl) {
788
- this.isExternalUrl.set(true);
789
- this.form.patchValue({
790
- version: this.version ?? '',
791
- deviceType: this.deviceType ?? '',
792
- description: this.description ?? '',
793
- uploadChoice: 'uploadUrl'
794
- });
795
- }
796
- else {
797
- const patchValues = {
798
- version: this.version ?? '',
799
- deviceType: this.deviceType ?? '',
800
- description: this.description ?? '',
801
- uploadChoice: 'editInline'
802
- };
803
- if (this.binary?.file) {
804
- this.existingBinary = this.binary.file;
805
- if (await isBinaryFile(this.binary.file)) {
806
- patchValues.uploadChoice = 'uploadBinary';
807
- }
808
- else {
809
- patchValues.editorContent = await this.binary.file.text();
810
- patchValues.editorFilename = this.binary.file.name;
811
- }
812
- }
813
- this.form.patchValue(patchValues);
814
- }
815
- this.form.markAsDirty();
816
- }
817
- this.cdr.markForCheck();
818
- }
819
- cancel() {
820
- this.settled = true;
821
- this._cancel('User canceled');
822
- this.bottomDrawerRef.close();
823
- }
824
- setPipe(filterStr) {
825
- this.pattern.set(filterStr);
826
- this.filterPipe = pipe(map(data => uniqBy(data, 'configurationType')), map(data => data.filter(mo => mo.configurationType &&
827
- mo.configurationType.toLowerCase().indexOf(filterStr.toLowerCase()) > -1)));
828
- }
829
- onUploadChoiceChange(choice) {
830
- this.form.controls.uploadChoice.setValue(choice);
831
- this.form.markAsDirty();
832
- }
833
- onFileBinaryChange(file) {
834
- this.binary = { ...this.binary, file };
835
- this.form.markAsDirty();
836
- this.cdr.markForCheck();
837
- }
838
- onFileUrlChange(url) {
839
- this.binary = { ...this.binary, url };
840
- this.form.markAsDirty();
841
- this.cdr.markForCheck();
842
- }
843
- onTextFilePicked({ name, content }) {
844
- this.form.patchValue({
845
- uploadChoice: 'editInline',
846
- editorFilename: name,
847
- editorContent: content
848
- });
849
- this.form.markAsDirty();
850
- this.cdr.markForCheck();
851
- }
852
- get isSaveDisabled() {
853
- if (this.saving() || !this.form.valid || this.form.pristine) {
854
- return true;
855
- }
856
- const { uploadChoice, editorFilename, editorContent } = this.form.value;
857
- if (uploadChoice === 'uploadBinary') {
858
- return !this.binary?.file;
859
- }
860
- if (uploadChoice === 'uploadUrl') {
861
- return !this.binary?.url?.trim();
862
- }
863
- if (uploadChoice === 'editInline') {
864
- return !editorFilename?.trim() || !editorContent;
865
- }
866
- return false;
867
- }
868
- async save() {
869
- if (this.isSaveDisabled) {
870
- return;
871
- }
872
- try {
873
- this.saving.set(true);
874
- const { version, deviceType, description, uploadChoice, editorFilename, editorContent } = this.form.value;
875
- let binary = { ...this.binary };
876
- if (uploadChoice === 'uploadUrl') {
877
- binary = { url: this.binary?.url };
878
- }
879
- else if (uploadChoice === 'editInline') {
880
- binary = {
881
- file: new File([editorContent], editorFilename, { type: mimeForFilename(editorFilename) })
882
- };
883
- }
884
- else if (this.existingBinary === this.binary?.file) {
885
- binary = { ...this.binary, file: undefined };
886
- }
887
- await this.repositoryService.create({
888
- version,
889
- description,
890
- binary,
891
- deviceType,
892
- configurationType: this.configurationTypeMO?.configurationType
893
- }, RepositoryType.CONFIGURATION, this.mo);
894
- this.alert.success(this.mo?.id ? gettext('Configuration updated.') : gettext('Configuration created.'));
895
- this.settled = true;
896
- this._save();
897
- this.bottomDrawerRef.close();
898
- }
899
- catch (ex) {
900
- // Keep the drawer open so the user can fix and retry.
901
- // Do NOT settle the result promise here.
902
- this.alert.addServerFailure(ex);
903
- }
904
- finally {
905
- this.saving.set(false);
906
- }
907
- }
908
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
909
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ConfigurationDetailComponent, isStandalone: true, selector: "c8y-configuration-detail", inputs: { version: "version", description: "description", binary: "binary", deviceType: "deviceType", mo: "mo" }, host: { classAttribute: "d-contents" }, ngImport: i0, template: "<div class=\"card-header j-c-center separator flex-no-shrink p-24\">\n <i\n class=\"m-r-8\"\n [c8yIcon]=\"'cogs'\"\n aria-hidden=\"true\"\n ></i>\n <h4 id=\"drawerTitle\">\n @if (mo?.id) {\n {{ 'Update configuration' | translate }}\n } @else {\n {{ 'Add configuration' | translate }}\n }\n </h4>\n</div>\n\n<form\n class=\"d-contents\"\n [formGroup]=\"form\"\n (ngSubmit)=\"save()\"\n>\n <div\n class=\"card-body inner-scroll p-24\"\n [class.d-col]=\"form.value.uploadChoice === 'editInline'\"\n >\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confVersion\"\n translate\n >\n Name\n </label>\n <input\n class=\"form-control\"\n id=\"confVersion\"\n placeholder=\"{{ 'e.g.' | translate }} hosts\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"version\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDeviceType\"\n translate\n >\n Device type\n </label>\n <input\n class=\"form-control\"\n id=\"confDeviceType\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"deviceType\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDescription\"\n translate\n >\n Description\n </label>\n <input\n class=\"form-control\"\n id=\"confDescription\"\n placeholder=\"{{ 'e.g.' | translate }} Host configuration\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"description\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label translate>Configuration type</label>\n <c8y-typeahead\n placeholder=\"{{ 'e.g.' | translate }} ssh\"\n name=\"confType\"\n data-cy=\"conf-detail--configuration-type\"\n [ngModel]=\"configurationTypeMO\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"configurationTypeMO = $event\"\n maxlength=\"254\"\n (onSearch)=\"setPipe($event)\"\n displayProperty=\"configurationType\"\n >\n @let patternStr = pattern();\n <c8y-li\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n *c8yFor=\"let config of configs; pipe: filterPipe; notFound: notFoundTemplate\"\n (click)=\"configurationTypeMO = config; setPipe('')\"\n [active]=\"configurationTypeMO === config\"\n >\n <c8y-highlight\n [text]=\"config.configurationType || '--'\"\n [pattern]=\"patternStr\"\n ></c8y-highlight>\n </c8y-li>\n <ng-template #notFoundTemplate>\n @if (patternStr.length > 0) {\n <c8y-li class=\"bg-level-2 p-8\">\n <span translate>No match found.</span>\n <button\n class=\"btn btn-primary btn-xs m-l-8\"\n title=\"{{ 'Add new`configuration type`' | translate }}\"\n type=\"button\"\n translate\n >\n Add new`configuration type`\n </button>\n </c8y-li>\n }\n </ng-template>\n </c8y-typeahead>\n </c8y-form-group>\n </div>\n\n <fieldset\n class=\"d-col\"\n [class.flex-grow]=\"form.value.uploadChoice === 'editInline'\"\n [class.min-height-0]=\"form.value.uploadChoice === 'editInline'\"\n >\n <legend\n class=\"legend form-block m-t-40\"\n translate\n >\n Configuration file\n </legend>\n\n @if (isExternalUrl()) {\n <p\n class=\"text-muted m-b-8\"\n translate\n >\n Content is sourced from a remote URL and cannot be edited inline.\n </p>\n <c8y-form-group>\n <label\n for=\"confFileUrl\"\n translate\n >\n File URL\n </label>\n <input\n class=\"form-control\"\n id=\"confFileUrl\"\n placeholder=\"https://...\"\n type=\"url\"\n autocomplete=\"off\"\n [ngModel]=\"binary.url\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"binary = { url: $event }; form.markAsDirty()\"\n />\n </c8y-form-group>\n } @else {\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n [allowedUploadChoices]=\"['uploadBinary', 'uploadUrl', 'editInline']\"\n [uploadChoice]=\"form.controls.uploadChoice.value\"\n (uploadChoiceChange)=\"onUploadChoiceChange($event)\"\n [fileUrl]=\"binary.url\"\n (fileUrlChange)=\"onFileUrlChange($event)\"\n [fileBinary]=\"binary.file\"\n (fileBinaryChange)=\"onFileBinaryChange($event)\"\n (textFilePicked)=\"onTextFilePicked($event)\"\n [fileUrlPopover]=\"textForConfigurationUrlPopover\"\n >\n <ng-container c8yInlineEditor>\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-9\">\n <label\n for=\"confEditorFilename\"\n translate\n >\n Filename\n </label>\n <input\n class=\"form-control\"\n id=\"confEditorFilename\"\n placeholder=\"config.json\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"editorFilename\"\n />\n </c8y-form-group>\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confEditorLanguage\"\n translate\n >\n Language\n </label>\n <c8y-select\n aria-label=\"{{ 'Editor language' | translate }}\"\n id=\"confEditorLanguage\"\n [items]=\"editorLanguages\"\n formControlName=\"editorLanguage\"\n ></c8y-select>\n </c8y-form-group>\n </div>\n <c8y-form-group class=\"d-col flex-grow min-height-0 m-b-0\">\n <label\n for=\"confEditorContent\"\n translate\n >\n Content\n </label>\n <c8y-editor\n class=\"flex-grow\"\n style=\"min-height: 95px\"\n id=\"confEditorContent\"\n formControlName=\"editorContent\"\n monacoEditorMarkerValidator\n [editorOptions]=\"{ language: editorLanguage() }\"\n ></c8y-editor>\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </ng-container>\n </c8y-file-picker>\n }\n </fieldset>\n </div>\n\n <div class=\"card-footer j-c-between separator flex-no-shrink p-24\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"cancel()\"\n [disabled]=\"saving()\"\n >\n <span translate>Cancel</span>\n </button>\n <button\n class=\"btn btn-primary\"\n [class.btn-pending]=\"saving()\"\n [title]=\"submitButtonTitle() | translate\"\n type=\"submit\"\n [disabled]=\"isSaveDisabled\"\n >\n {{ submitButtonTitle() | translate }}\n </button>\n </div>\n</form>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i4$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4$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: i4$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i4$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i4$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: TypeaheadComponent, selector: "c8y-typeahead", inputs: ["required", "maxlength", "disabled", "allowFreeEntries", "placeholder", "displayProperty", "icon", "name", "autoClose", "hideNew", "container", "selected", "title", "highlightFirstItem"], outputs: ["onSearch", "onIconClick"] }, { kind: "directive", type: ForOfDirective, selector: "[c8yFor]", inputs: ["c8yForOf", "c8yForLoadMore", "c8yForPipe", "c8yForNotFound", "c8yForMaxIterations", "c8yForLoadingTemplate", "c8yForLoadNextLabel", "c8yForLoadingLabel", "c8yForRealtime", "c8yForRealtimeOptions", "c8yForComparator", "c8yForEnableVirtualScroll", "c8yForVirtualScrollElementSize", "c8yForVirtualScrollStrategy", "c8yForVirtualScrollContainerHeight"], outputs: ["c8yForCount", "c8yForChange", "c8yForLoadMoreComponent"] }, { kind: "component", type: ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: HighlightComponent, selector: "c8y-highlight", inputs: ["pattern", "text", "elementClass", "shouldTrimPattern"] }, { kind: "component", type: FilePickerComponent, selector: "c8y-file-picker", inputs: ["maxAllowedFiles", "uploadChoice", "allowedUploadChoices", "fileUrl", "fileBinary", "config", "filePickerIndex", "fileUrlPopover", "editInlineConfirmTitle", "editInlineConfirmMessage"], outputs: ["onFilesPicked", "uploadChoiceChange", "fileBinaryChange", "fileUrlChange", "textFilePicked"] }, { kind: "component", type: EditorComponent, selector: "c8y-editor", inputs: ["editorOptions", "theme"], outputs: ["editorInit"] }, { kind: "directive", type: MonacoEditorMarkerValidatorDirective, selector: "c8y-editor [monacoEditorMarkerValidator]" }, { kind: "component", type: MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage", "additionalMessages"] }, { kind: "directive", type: RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { 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" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
910
- }
911
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ConfigurationDetailComponent, decorators: [{
912
- type: Component,
913
- args: [{ selector: 'c8y-configuration-detail', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-contents' }, imports: [
914
- ReactiveFormsModule,
915
- FormsModule,
916
- IconDirective,
917
- C8yTranslateDirective,
918
- FormGroupComponent,
919
- TypeaheadComponent,
920
- ForOfDirective,
921
- ListItemComponent,
922
- HighlightComponent,
923
- FilePickerComponent,
924
- NgClass,
925
- C8yTranslatePipe,
926
- EditorComponent,
927
- MonacoEditorMarkerValidatorDirective,
928
- MessagesComponent,
929
- RequiredInputPlaceholderDirective,
930
- SelectComponent
931
- ], template: "<div class=\"card-header j-c-center separator flex-no-shrink p-24\">\n <i\n class=\"m-r-8\"\n [c8yIcon]=\"'cogs'\"\n aria-hidden=\"true\"\n ></i>\n <h4 id=\"drawerTitle\">\n @if (mo?.id) {\n {{ 'Update configuration' | translate }}\n } @else {\n {{ 'Add configuration' | translate }}\n }\n </h4>\n</div>\n\n<form\n class=\"d-contents\"\n [formGroup]=\"form\"\n (ngSubmit)=\"save()\"\n>\n <div\n class=\"card-body inner-scroll p-24\"\n [class.d-col]=\"form.value.uploadChoice === 'editInline'\"\n >\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confVersion\"\n translate\n >\n Name\n </label>\n <input\n class=\"form-control\"\n id=\"confVersion\"\n placeholder=\"{{ 'e.g.' | translate }} hosts\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"version\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDeviceType\"\n translate\n >\n Device type\n </label>\n <input\n class=\"form-control\"\n id=\"confDeviceType\"\n placeholder=\"{{ 'e.g.' | translate }} c8y_Linux\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"deviceType\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confDescription\"\n translate\n >\n Description\n </label>\n <input\n class=\"form-control\"\n id=\"confDescription\"\n placeholder=\"{{ 'e.g.' | translate }} Host configuration\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"description\"\n />\n </c8y-form-group>\n\n <c8y-form-group class=\"col-sm-3\">\n <label translate>Configuration type</label>\n <c8y-typeahead\n placeholder=\"{{ 'e.g.' | translate }} ssh\"\n name=\"confType\"\n data-cy=\"conf-detail--configuration-type\"\n [ngModel]=\"configurationTypeMO\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"configurationTypeMO = $event\"\n maxlength=\"254\"\n (onSearch)=\"setPipe($event)\"\n displayProperty=\"configurationType\"\n >\n @let patternStr = pattern();\n <c8y-li\n class=\"p-l-8 p-r-8 c8y-list__item--link\"\n *c8yFor=\"let config of configs; pipe: filterPipe; notFound: notFoundTemplate\"\n (click)=\"configurationTypeMO = config; setPipe('')\"\n [active]=\"configurationTypeMO === config\"\n >\n <c8y-highlight\n [text]=\"config.configurationType || '--'\"\n [pattern]=\"patternStr\"\n ></c8y-highlight>\n </c8y-li>\n <ng-template #notFoundTemplate>\n @if (patternStr.length > 0) {\n <c8y-li class=\"bg-level-2 p-8\">\n <span translate>No match found.</span>\n <button\n class=\"btn btn-primary btn-xs m-l-8\"\n title=\"{{ 'Add new`configuration type`' | translate }}\"\n type=\"button\"\n translate\n >\n Add new`configuration type`\n </button>\n </c8y-li>\n }\n </ng-template>\n </c8y-typeahead>\n </c8y-form-group>\n </div>\n\n <fieldset\n class=\"d-col\"\n [class.flex-grow]=\"form.value.uploadChoice === 'editInline'\"\n [class.min-height-0]=\"form.value.uploadChoice === 'editInline'\"\n >\n <legend\n class=\"legend form-block m-t-40\"\n translate\n >\n Configuration file\n </legend>\n\n @if (isExternalUrl()) {\n <p\n class=\"text-muted m-b-8\"\n translate\n >\n Content is sourced from a remote URL and cannot be edited inline.\n </p>\n <c8y-form-group>\n <label\n for=\"confFileUrl\"\n translate\n >\n File URL\n </label>\n <input\n class=\"form-control\"\n id=\"confFileUrl\"\n placeholder=\"https://...\"\n type=\"url\"\n autocomplete=\"off\"\n [ngModel]=\"binary.url\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"binary = { url: $event }; form.markAsDirty()\"\n />\n </c8y-form-group>\n } @else {\n <c8y-file-picker\n [maxAllowedFiles]=\"1\"\n [allowedUploadChoices]=\"['uploadBinary', 'uploadUrl', 'editInline']\"\n [uploadChoice]=\"form.controls.uploadChoice.value\"\n (uploadChoiceChange)=\"onUploadChoiceChange($event)\"\n [fileUrl]=\"binary.url\"\n (fileUrlChange)=\"onFileUrlChange($event)\"\n [fileBinary]=\"binary.file\"\n (fileBinaryChange)=\"onFileBinaryChange($event)\"\n (textFilePicked)=\"onTextFilePicked($event)\"\n [fileUrlPopover]=\"textForConfigurationUrlPopover\"\n >\n <ng-container c8yInlineEditor>\n <div class=\"row\">\n <c8y-form-group class=\"col-sm-9\">\n <label\n for=\"confEditorFilename\"\n translate\n >\n Filename\n </label>\n <input\n class=\"form-control\"\n id=\"confEditorFilename\"\n placeholder=\"config.json\"\n aria-required=\"true\"\n type=\"text\"\n autocomplete=\"off\"\n formControlName=\"editorFilename\"\n />\n </c8y-form-group>\n <c8y-form-group class=\"col-sm-3\">\n <label\n for=\"confEditorLanguage\"\n translate\n >\n Language\n </label>\n <c8y-select\n aria-label=\"{{ 'Editor language' | translate }}\"\n id=\"confEditorLanguage\"\n [items]=\"editorLanguages\"\n formControlName=\"editorLanguage\"\n ></c8y-select>\n </c8y-form-group>\n </div>\n <c8y-form-group class=\"d-col flex-grow min-height-0 m-b-0\">\n <label\n for=\"confEditorContent\"\n translate\n >\n Content\n </label>\n <c8y-editor\n class=\"flex-grow\"\n style=\"min-height: 95px\"\n id=\"confEditorContent\"\n formControlName=\"editorContent\"\n monacoEditorMarkerValidator\n [editorOptions]=\"{ language: editorLanguage() }\"\n ></c8y-editor>\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </ng-container>\n </c8y-file-picker>\n }\n </fieldset>\n </div>\n\n <div class=\"card-footer j-c-between separator flex-no-shrink p-24\">\n <button\n class=\"btn btn-default\"\n title=\"{{ 'Cancel' | translate }}\"\n type=\"button\"\n (click)=\"cancel()\"\n [disabled]=\"saving()\"\n >\n <span translate>Cancel</span>\n </button>\n <button\n class=\"btn btn-primary\"\n [class.btn-pending]=\"saving()\"\n [title]=\"submitButtonTitle() | translate\"\n type=\"submit\"\n [disabled]=\"isSaveDisabled\"\n >\n {{ submitButtonTitle() | translate }}\n </button>\n </div>\n</form>\n" }]
932
- }], ctorParameters: () => [], propDecorators: { version: [{
933
- type: Input
934
- }], description: [{
935
- type: Input
936
- }], binary: [{
937
- type: Input
938
- }], deviceType: [{
939
- type: Input
940
- }], mo: [{
941
- type: Input
942
- }] } });
943
-
944
1020
  class ConfigurationListComponent {
945
1021
  constructor() {
946
1022
  this.alert = inject(AlertService);