diff --git a/projects/app-ziti-console/src/app/app-routing.module.ts b/projects/app-ziti-console/src/app/app-routing.module.ts index 70510e29..4f804413 100644 --- a/projects/app-ziti-console/src/app/app-routing.module.ts +++ b/projects/app-ziti-console/src/app/app-routing.module.ts @@ -96,7 +96,7 @@ const routes: Routes = [ }, { path: 'configs', - component: ZacWrapperComponent, + component: ConfigurationsPageComponent, canActivate: mapToCanActivate([AuthenticationGuard]), runGuardsAndResolvers: 'always', }, diff --git a/projects/ziti-console-lib/src/lib/assets/styles/forms.css b/projects/ziti-console-lib/src/lib/assets/styles/forms.css index ceef67c2..53341da5 100644 --- a/projects/ziti-console-lib/src/lib/assets/styles/forms.css +++ b/projects/ziti-console-lib/src/lib/assets/styles/forms.css @@ -12,6 +12,7 @@ label { font-family: 'Open Sans'; overflow: hidden; text-overflow: ellipsis; + line-height: var(--defaultLineHeight); } textarea::placeholder { diff --git a/projects/ziti-console-lib/src/lib/assets/styles/ziti.css b/projects/ziti-console-lib/src/lib/assets/styles/ziti.css index 2d9c47c4..80cfcaa6 100644 --- a/projects/ziti-console-lib/src/lib/assets/styles/ziti.css +++ b/projects/ziti-console-lib/src/lib/assets/styles/ziti.css @@ -39,6 +39,7 @@ body { --gapLarge: 10px; --gapXL: 15px; --defaultInputHeight: 43px; + --defaultLineHeight: 15px; } ::placeholder { diff --git a/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.html b/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.html new file mode 100644 index 00000000..93f77074 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.html @@ -0,0 +1,5 @@ +
+ +
+ + \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.scss b/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.spec.ts b/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.spec.ts new file mode 100644 index 00000000..c707ed56 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfigEditorComponent } from './config-editor.component'; + +describe('ConfigEditorComponent', () => { + let component: ConfigEditorComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ConfigEditorComponent] + }); + fixture = TestBed.createComponent(ConfigEditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.ts b/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.ts new file mode 100644 index 00000000..5418a426 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/features/config-editor/config-editor.component.ts @@ -0,0 +1,224 @@ +import {Component, ComponentRef, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef} from '@angular/core'; +import {JsonEditorComponent, JsonEditorOptions} from "ang-jsoneditor"; +import {defer, isBoolean, isEmpty, isNil, keys} from "lodash"; +import {SchemaService} from "../../services/schema.service"; + +@Component({ + selector: 'lib-config-editor', + templateUrl: './config-editor.component.html', + styleUrls: ['./config-editor.component.scss'] +}) +export class ConfigEditorComponent implements OnInit { + + items: any[] = []; + + lColorArray = [ + 'white', + 'white', + 'white', + ] + + bColorArray = [ + 'var(--formBase)', + 'var(--formGroup)', + 'var(--formSubGroup)' + ] + hideConfigJSON = false; + + @Input() configErrors: any = {}; + @Output() configErrorsChange = new EventEmitter(); + + _configData: any = {}; + @Input() set configData(data) { + this._configData = data; + }; + @Output() configDataChange = new EventEmitter(); + get configData() { + return this._configData; + } + + _showJsonView = false; + @Input() set showJsonView(showJsonView: boolean) { + this._showJsonView = showJsonView; + this.updateConfigData(); + } + get showJsonView() { + return this._showJsonView; + } + + _schema = false; + @Input() set schema(schema: any) { + this._schema = schema; + this.createForm(); + this.updateConfigData(); + } + + get schema() { + return this._schema; + } + + @ViewChild("dynamicform", {read: ViewContainerRef}) dynamicForm!: ViewContainerRef; + @ViewChild(JsonEditorComponent, {static: false}) editor!: JsonEditorComponent; + constructor(private schemaSvc: SchemaService) { + } + + ngOnInit() { + this.createForm(); + } + + createForm() { + this.clearForm(); + if (this.dynamicForm && this.schema) { + this.renderSchema(this.schema); + } + } + + clearForm() { + this.items.forEach((item: any) => { + if (item?.component) item.component.destroy(); + }); + this.configErrors = {}; + this.items = []; + } + + renderSchema(schema: any) { + if (schema.properties) { + this.items = this.schemaSvc.render(schema, this.dynamicForm, this.lColorArray, this.bColorArray); + for (let obj of this.items) { + const cRef = obj.component; + cRef.instance.errors = this.configErrors; + if (cRef?.instance.valueChange) { + const pName: string[] = cRef.instance.parentage; + let parentKey; + if(pName) parentKey = pName.join('.'); + //if (parentKey && !this.formData[parentKey]) this.formData[parentKey] = {}; + } + } + } + } + + updateConfigData() { + if (!this.showJsonView) { + this.updateFormView(this.items, this.configData); + } else { + this.hideConfigJSON = true; + defer(() => { + this.getConfigDataFromForm(); + }); + } + } + + updateFormView(items, data: any = {}) { + items.forEach((item) => { + if (item.items) { + if (item.type === 'array') { + if (item?.component?.instance?.addedItems) { + item.addedItems = data[item.key] || []; + item.component.instance.addedItems = data[item.key] || []; + } + this.updateFormView(item.items, {}); + } else { + this.updateFormView(item.items, data[item.key]); + } + } else if (item?.component?.instance?.setProperties) { + let val; + switch (item.key) { + case 'forwardingconfig': + val = { + protocol: data.protocol, + address: data.address, + port: data.port, + forwardProtocol: data.forwardProtocol, + forwardAddress: data.forwardAddress, + forwardPort: data.forwardPort, + allowedProtocols: data.allowedProtocols, + allowedAddresses: data.allowedAddresses, + allowedPortRanges: data.allowedPortRanges + } + break; + case 'pap': + val = { + protocol: data.protocol, + address: data.address, + port: data.port, + hostname: data.hostname, + } + break; + default: + val = data[item.key]; + break; + } + item?.component?.instance?.setProperties(val); + } else if (item?.component?.setInput) { + item.component.setInput('fieldValue', data[item.key]); + } + }); + return data; + } + + getConfigDataFromForm() { + const data = {}; + this.addItemsToConfig(this.items, data); + this._configData = data; + this.hideConfigJSON = false; + this.configDataChange.emit(this.configData); + } + + addItemsToConfig(items, data: any = {}, parentType = 'object') { + items.forEach((item) => { + if (item.type === 'array') { + if (item.addedItems) { + data[item.key] = item.addedItems; + } else { + data[item.key] = []; + } + } else if (item.type === 'object') { + const val = this.addItemsToConfig(item.items, {}, item.type); + let hasDefinition = false; + keys(val).forEach((key) => { + if (isBoolean(val[key]) || (!isEmpty(val[key]) && !isNil(val[key]))) { + hasDefinition = true; + } + }); + data[item.key] = hasDefinition ? val : undefined; + } else { + let props = []; + if (item?.component?.instance?.getProperties) { + props = item?.component?.instance?.getProperties(); + } else if (item?.component?.instance) { + props = [{key: item.key, value: item.component.instance.fieldValue}]; + } + props.forEach((prop) => { + data[prop.key] = prop.value; + }); + } + }); + return data; + } + + validateConfig() { + this.configErrors = {}; + const configItemsValid = this.validateConfigItems(this.items); + if (!configItemsValid) { + this.configErrors['configData'] = true; + } + this.configErrorsChange.emit(this.configErrors); + return isEmpty(this.configErrors); + } + + validateConfigItems(items, parentType = 'object') { + let isValid = true; + items.forEach((item) => { + if (item.type === 'object') { + if (!this.validateConfigItems(item.items, item.type)) { + isValid = false; + } + } else if (item?.component?.instance?.isValid) { + if (!item?.component?.instance?.isValid()) { + isValid = false; + } + } + }); + return isValid; + } +} diff --git a/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-default/table-column-default.component.ts b/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-default/table-column-default.component.ts index 249e1cc3..38d1e571 100644 --- a/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-default/table-column-default.component.ts +++ b/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-default/table-column-default.component.ts @@ -136,11 +136,7 @@ export class TableColumnDefaultComponent implements IHeaderAngularComp, AfterVie this.columnDef.colId === 'name' && (_.has(this.columnFilters, 'hasApiSession') || _.has(this.columnFilters, 'online')); this._showColumnMenu = headerParams.showColumnMenu; - this.filterOptions = _.map(this.headerParams.filterOptions, (option) => { - option.columnId = this.columnDef.colId; - option.filterName = this.headerName; - return option; - }); + this.updateFilterOptions(); headerParams.api.addEventListener('sortChanged', (event) => { const colId = _.get(event, 'columnDef.colId', ''); if (colId !== this.columnDef.colId) { @@ -152,11 +148,7 @@ export class TableColumnDefaultComponent implements IHeaderAngularComp, AfterVie headerParams.api.addEventListener('columnEverythingChanged', (event) => { _.forEach(event.columnApi.columnModel.columnDefs, colDef => { if (this.columnDef.colId === colDef.colId && colDef.headerComponentParams?.filterOptions) { - this.filterOptions = _.map(colDef.headerComponentParams?.filterOptions, (option) => { - option.columnId = this.columnDef.colId; - option.filterName = this.headerName; - return option; - }); + this.updateFilterOptions(); } }); }); @@ -213,12 +205,14 @@ export class TableColumnDefaultComponent implements IHeaderAngularComp, AfterVie this.showFilter = !this.showFilter; if (this.filterType === 'SELECT' || this.filterType === 'COMBO' || this.filterType === 'DATETIME') { if (this.showFilter) { - _.invoke(this.headerParams, 'api.openHeaderFilter', event, this.filterOptions, this.filterType, this.columnDef?.colId); + this.updateFilterOptions(); + _.invoke(this.headerParams, 'api.openHeaderFilter', event, this.filterOptions, this.filterType, this.columnDef?.colId, this.headerName); } else { _.invoke(this.headerParams, 'api.closeHeaderFilter', event); } } else if (this.filterType === 'CUSTOM') { if (this.showFilter) { + this.updateFilterOptions(); _.invoke(this.headerParams, 'column.colDef.headerComponentParams.openHeaderFilter', event, this.filterOptions); } else { _.invoke(this.headerParams, 'column.colDef.headerComponentParams.closeHeaderFilter', event); @@ -287,4 +281,19 @@ export class TableColumnDefaultComponent implements IHeaderAngularComp, AfterVie } this._showColumnMenu(this.menuButton.nativeElement); } + + updateFilterOptions() { + let options = []; + if (this.headerParams.getFilterOptions) { + options = this.headerParams.getFilterOptions(); + } else { + options = this.headerParams.filterOptions; + } + this.filterOptions = _.map(options, (option) => { + option.columnId = this.columnDef.colId; + option.filterName = this.headerName; + option.type = this.filterType; + return option; + }); + } } diff --git a/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-filter/table-column-filter.component.ts b/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-filter/table-column-filter.component.ts index eff18191..afa5d4ea 100644 --- a/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-filter/table-column-filter.component.ts +++ b/projects/ziti-console-lib/src/lib/features/data-table/column-headers/table-column-filter/table-column-filter.component.ts @@ -60,6 +60,7 @@ export class TableColumnFilterComponent implements OnInit, AfterViewInit, OnDest columnId: this.columnId, value: this.filterString, label: this.filterString, + type: this.type, }; this.filterService.updateFilter(filterObj) } diff --git a/projects/ziti-console-lib/src/lib/features/data-table/data-table-filter.service.ts b/projects/ziti-console-lib/src/lib/features/data-table/data-table-filter.service.ts index 1d56eb29..1fd0a9c3 100644 --- a/projects/ziti-console-lib/src/lib/features/data-table/data-table-filter.service.ts +++ b/projects/ziti-console-lib/src/lib/features/data-table/data-table-filter.service.ts @@ -7,6 +7,7 @@ export type FilterObj = { columnId: string; value: any; label: string; + type?: string; } @Injectable({ diff --git a/projects/ziti-console-lib/src/lib/features/data-table/data-table.component.html b/projects/ziti-console-lib/src/lib/features/data-table/data-table.component.html index 67e8f49e..28c3db46 100644 --- a/projects/ziti-console-lib/src/lib/features/data-table/data-table.component.html +++ b/projects/ziti-console-lib/src/lib/features/data-table/data-table.component.html @@ -88,7 +88,7 @@ -
+
{ this.calendar.toggle(); }, 100); @@ -355,8 +357,8 @@ export class DataTableComponent implements OnChanges, OnInit { label = 'Last Day'; break; } - const startDateRange = encodeURIComponent('>=' + startDate.toISOString()); - const endDateRange = encodeURIComponent('<=' + endDate.toISOString()); + const startDateRange = encodeURIComponent(startDate.toISOString()); + const endDateRange = encodeURIComponent(endDate.toISOString()); this.columnFilters[this.dateTimeColumn] = [startDateRange, endDateRange]; if (closeCalendar) { @@ -369,7 +371,8 @@ export class DataTableComponent implements OnChanges, OnInit { columnId: this.dateTimeColumn, value: [startDateRange, endDateRange], label: label, - filterName: 'Last Seen' + filterName: this.dateTimeFilterLabel, + type: 'DATETIME' }; this.tableFilterService.updateFilter(filterObj); diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.html b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.html index 5c99e1b9..2162a13b 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.html +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.html @@ -1,22 +1,102 @@ -
- - - - - - - +
+ +
+
+
+ + + + +
+ Configuration Data +
+ + FORM + +
+
+
+
+
+
+ + JSON + +
+
+
+
+ {{isEditing ? 'Config Type' : 'Select a Config Type'}} +
+ + +
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+
+ diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.scss b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.scss index e69de29b..0109b841 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.scss +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.scss @@ -0,0 +1,15 @@ +.config-form-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + align-items: center; + gap: var(--gapXL); +} + +.config-type-select { + &.disabled { + opacity: .7; + pointer-events: none; + } +} \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.ts index 6b0db59a..76e83f7a 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.ts +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration-form.component.ts @@ -16,24 +16,23 @@ import { Component, - ComponentRef, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, ViewChild, - ViewContainerRef } from '@angular/core'; import {ConfigurationService} from "./configuration.service"; import {Subscription} from "rxjs"; import {SchemaService} from "../../../services/schema.service"; import {ProjectableForm} from "../projectable-form.class"; -import {JsonEditorComponent, JsonEditorOptions} from 'ang-jsoneditor'; -import _ from "lodash"; import {GrowlerService} from "../../messaging/growler.service"; import {ExtensionService, SHAREDZ_EXTENSION} from "../../extendable/extensions-noop.service"; -import {SERVICE_EXTENSION_SERVICE} from "../service/service-form.service"; +import {ConfigEditorComponent} from "../../config-editor/config-editor.component"; +import {cloneDeep, defer, isEmpty} from 'lodash'; +import {GrowlerModel} from "../../messaging/growler.model"; +import {SETTINGS_SERVICE, SettingsService} from "../../../services/settings.service"; @Component({ selector: 'lib-configuration', @@ -41,68 +40,89 @@ import {SERVICE_EXTENSION_SERVICE} from "../service/service-form.service"; styleUrls: ['./configuration-form.component.scss'] }) export class ConfigurationFormComponent extends ProjectableForm implements OnInit, OnDestroy { - @ViewChild("dynamicform", {read: ViewContainerRef}) dynamicForm!: ViewContainerRef; - @ViewChild(JsonEditorComponent, {static: false}) editor!: JsonEditorComponent; @Input() override formData: any = {}; @Input() override errors: any = {}; - @Output() currentSchema = new EventEmitter(); - @Output() showButtons = new EventEmitter(); @Output() close: EventEmitter = new EventEmitter(); - options: string[] = []; - - lColorArray = [ - 'black', - 'white', - 'white', - ] - - bColorArray = [ - '#33aaff', - '#fafafa', - '#33aaff', - ] - - configType: string = ''; + isLoading = false; + options: any[] = []; + isEditing = !isEmpty(this.formData.id); + formView = 'simple'; + formDataInvalid = false; editMode = false; items: any = []; - subscription = new Subscription() - editorOptions: JsonEditorOptions; - jsonData: any; - onChangeDebounced = _.debounce(this.onJsonChange.bind(this), 400); - private schema: any; - private readOnly: false; - - constructor(private svc: ConfigurationService, - private schemaSvc: SchemaService, - growlerService: GrowlerService, - @Inject(SHAREDZ_EXTENSION) extService: ExtensionService) { + subscription = new Subscription(); + selectedSchema: any = ''; + associatedServices = []; + associatedServiceNames = []; + servicesLoading = false; + settings: any = {}; + + @ViewChild("configEditor", {read: ConfigEditorComponent}) configEditor!: ConfigEditorComponent; + constructor( + public svc: ConfigurationService, + private schemaSvc: SchemaService, + growlerService: GrowlerService, + @Inject(SHAREDZ_EXTENSION) extService: ExtensionService, + @Inject(SETTINGS_SERVICE) public settingsService: SettingsService, + ) { super(growlerService, extService); } - async createForm() { - this.clearForm(); - if (this.configType && this.dynamicForm) { - this.schema = await this.svc.getSchema(this.configType); - if (this.schema) { - this.currentSchema.emit(this.schema); - this.render(this.schema); - } + ngOnInit(): void { + this.settingsService.settingsChange.subscribe((results:any) => { + this.settings = results; + }); + this.initData = cloneDeep(this.formData); + this.svc.configJsonView = false; + //this.getAssociatedServices(); + this.svc.getConfigTypes() + .then(configTypes => { + this.options = configTypes.sort(); + this.getSchema(); + }); + this.isEditing = !isEmpty(this.formData.id); + if (this.configTypeEmpty) { + this.formData.configTypeId = ''; } } + getAssociatedServices() { + this.svc.getAssociatedServices(this.formData.id).then((result) => { + this.associatedServices = result.associatedServices; + this.associatedServiceNames = result.associatedServiceNames; + }); + } + ngOnDestroy(): void { this.clearForm(); } - override clear() { - this.configType = ''; + get configTypeEmpty() { + return isEmpty(this.formData.configTypeId); + } + + headerActionRequested(event) { + switch(event.name) { + case 'save': + this.save(event); + break; + case 'close': + this.closeModal(true); + break; + case 'toggle-view': + this.formView = event.data; + break; + } + } + + override clear() { + this.formData.configTypeId = ''; this.clearForm(); } clearForm() { - this.showButtons.emit(false); this.items.forEach((item: any) => { if (item?.component) item.component.destroy(); }); @@ -112,81 +132,87 @@ export class ConfigurationFormComponent extends ProjectableForm implements OnIni if (this.subscription) this.subscription.unsubscribe(); } - render(schema: any) { - if (schema.properties) { - this.items = this.schemaSvc.render(schema, this.dynamicForm, this.lColorArray, this.bColorArray); - for (let obj of this.items) { - const cRef = obj.component; - cRef.instance.errors = this.errors; - if (cRef?.instance.valueChange) { - const pName: string[] = cRef.instance.parentage; - let parentKey; - if(pName) parentKey = pName.join('.'); - if (parentKey && !this.formData[parentKey]) this.formData[parentKey] = {}; - this.subscription.add( - cRef.instance.valueChange.subscribe((val: any) => { - this.setFormValue(cRef, val); - })); - } - } - - this.showButtons.emit(this.items.length > 0); + async save(event?: any) { + this.errors = {}; + this.isLoading = true; + try { + this.configEditor.getConfigDataFromForm(); + } catch (err){ + const growlerData = new GrowlerModel( + 'error', + 'Error', + `Error Getting Config Data`, + 'Error getting configuration data from from.', + ); + this.growlerService.show(growlerData); + throw err; } - } - - ngOnInit(): void { - this.editorOptions = this.svc.initJsonEditorOptions(); - this.editorOptions.schema = this.schema; - this.editorOptions.onEditable = () => !this.readOnly; - (this.editorOptions).onBlur = this.onJsonChange.bind(this); - this.svc.getConfigTypes() - .then(recs => { - this.options = recs.map(r => r.name).sort(); - }) - } - - - - private setFormValue(cRef: ComponentRef, val: any) { - const pName = cRef.instance.parentage; - const fName = cRef.instance.fieldName; - if (pName && !this.formData[pName]) this.formData[pName] = {}; - if(fName === 'pap') { - this.setSpecialFormValue(cRef, val, pName); - } else { - if (pName && !this.formData[pName]) this.formData[pName][fName] = val; - else this.formData[fName] = val; + if (!this.validate()) { + this.isLoading = false; + return; } + const configId = await this.svc.save(this.formData).then((result) => { + if (!isEmpty(result?.id)) { + this.formData = result; + this.initData = this.formData; + } + return result?.id; + }).finally(() => { + this.isLoading = false; + }); + if (configId) { + this.closeModal(true, true); + }; } - private setSpecialFormValue(cRef: ComponentRef, val: any, pName) { - const lPrefix = cRef.instance.labelPrefix - if (val.protocol) { - const fieldName = lPrefix ? lPrefix.trim().toLowerCase() + 'protocol' : 'protocol' - if (pName && !this.formData[pName]) this.formData[pName][fieldName] = val; - else this.formData[fieldName] = val; + validate() { + this.errors = {}; + this.configEditor.validateConfig(); + if (isEmpty(this.formData.name)) { + this.errors['name'] = true; } - if (val.address) { - const fieldName = lPrefix ? lPrefix.trim().toLowerCase() + 'address' : 'address' - if (pName && !this.formData[pName]) this.formData[pName][fieldName] = val; - else this.formData[fieldName] = val; + if (isEmpty(this.formData.configTypeId)) { + this.errors['configTypeId'] = true; } - if (val.port) { - const fieldName = lPrefix ? lPrefix.trim().toLowerCase() + 'port' : 'port' - if (pName && !this.formData[pName]) this.formData[pName][fieldName] = val; - else this.formData[fieldName] = val; + if (!isEmpty(this.errors)) { + const growlerData = new GrowlerModel( + 'error', + 'Error', + `Error Validating Config`, + 'The entered configuration is invalid. Please update missing/invalid fields and try again.', + ); + this.growlerService.show(growlerData); } + return isEmpty(this.errors); } - - onJsonChange() { - + get saveButtonTooltip() { + if (this.formDataInvalid) { + return 'Service data is invalid. Please update and try again.' + } else { + return 'Complete and attach config definition, or remove before saving'; + } } - onJsonChangeDebounced() { + async getSchema() { + this.formData.data = {}; + this.selectedSchema = await this.svc.getSchema(this.formData.configTypeId); + } + get apiCallURL() { + return this.settings.selectedEdgeController + '/edge/management/v1/configs' + (this.formData.id ? `/${this.formData.id}` : ''); } - save(): void { + get apiData(): any { + const data: any = { + name: this.formData?.name || '', + configTypeId: this.formData?.configTypeId || '', + data: this.formData?.data || {}, + }; + return data; } + _apiData = {}; + set apiData(data) { + this._apiData = data; + } } diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration.service.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration.service.ts index b7a2ab6d..3a8846a7 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration.service.ts +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/configuration/configuration.service.ts @@ -1,7 +1,13 @@ import {Injectable, Inject} from '@angular/core'; import {ZITI_DATA_SERVICE, ZitiDataService} from "../../../services/ziti-data.service"; import {Resolver} from "@stoplight/json-ref-resolver"; -import {JsonEditorOptions} from "ang-jsoneditor"; +import {SchemaService} from "../../../services/schema.service"; +import {defer, isBoolean, isEmpty, isNil, keys} from "lodash"; +import {GrowlerModel} from "../../messaging/growler.model"; +import {GrowlerService} from "../../messaging/growler.service"; +import {Config} from "../../../models/config"; +import {ExtensionService, SHAREDZ_EXTENSION} from "../../extendable/extensions-noop.service"; +import {SERVICE_EXTENSION_SERVICE} from "../service/service-form.service"; @Injectable({ providedIn: 'root' @@ -9,21 +15,42 @@ import {JsonEditorOptions} from "ang-jsoneditor"; export class ConfigurationService { private configTypes: any[] = []; - constructor(@Inject(ZITI_DATA_SERVICE) private dataService: ZitiDataService) { + items: any[] = []; + errors: any[] = []; + saveDisabled = false; + configJsonView = false; + configDataLabel = 'Configuration Form'; + + constructor( + @Inject(ZITI_DATA_SERVICE) private dataService: ZitiDataService, + @Inject(SHAREDZ_EXTENSION)private extService: ExtensionService, + private schemaSvc: SchemaService, + private growlerService: GrowlerService + ) { } async getSchema(schemaType: string): Promise { if (!schemaType) return Promise.resolve(); for (let idx = 0; idx < this.configTypes.length; idx++) { - if(this.configTypes[idx].name === schemaType) + if(this.configTypes[idx].id === schemaType) return this.configTypes[idx].schema; } } getConfigTypes() { - return this.dataService.get('config-types', {}, []) + const paging = { + filter: "", + noSearch: false, + order: "asc", + page: 1, + searchOn: "name", + sort: "name", + total: 50 + }; + return this.dataService.get('config-types', paging, []) .then(async (body: any) => { if (body.error) throw body.error; + this.configTypes = []; const promises: Promise[] = []; const resolver = new Resolver(); body.data.map( (row: any) => { @@ -36,11 +63,86 @@ export class ConfigurationService { return Promise.all(promises).then(() => this.configTypes); }); } - initJsonEditorOptions() { - const editorOptions = new JsonEditorOptions(); - editorOptions.modes = ['code', 'tree']; - editorOptions.mode = 'code'; - editorOptions.enableTransform = false; - return editorOptions + + getAssociatedServices(configId) { + return this.dataService.getSubdata('configs', configId, 'services').then((result: any) => { + const associatedServices = result.data; + const associatedServiceNames = associatedServices.map((svc) => { + return svc.name; + }); + return {associatedServices, associatedServiceNames}; + }); + } + + toggleJSONView() { + this.configJsonView = !this.configJsonView; + this.configDataLabel = this.configJsonView ? 'JSON Configuration' : 'Configuration Form'; + if (this.configJsonView) { + //this.configSubscriptions.unsubscribe(); + } + } + + save(formData) { + const isUpdate = !isEmpty(formData.id); + const data: any = this.getConfigDataModel(formData, isUpdate); + let prom; + if (isUpdate) { + prom = this.dataService.patch('configs', data, formData.id, true); + } else { + prom = this.dataService.post('configs', data, true); + } + + return prom.then(async (result: any) => { + const id = isUpdate ? formData.id : (result?.data?.id || result?.id); + let config = await this.dataService.getSubdata('configs', id, '').then((svcData) => { + return svcData.data; + }); + return this.extService.formDataSaved(config).then((formSavedResult: any) => { + if (!formSavedResult) { + return config; + } + const growlerData = new GrowlerModel( + 'success', + 'Success', + `Config ${isUpdate ? 'Updated' : 'Created'}`, + `Successfully ${isUpdate ? 'updated' : 'created'} Config: ${formData.name}`, + ); + this.growlerService.show(growlerData); + return config; + }).catch((result) => { + return false; + }); + }).catch((resp) => { + let errorMessage; + if (resp?.error?.error?.cause?.message) { + errorMessage = resp?.error?.error?.cause?.message; + } else if (resp?.error?.error?.cause?.reason) { + errorMessage = resp?.error?.error?.cause?.reason; + }else if (resp?.error?.message) { + errorMessage = resp?.error?.message; + } else { + errorMessage = 'An unknown error occurred'; + } + const growlerData = new GrowlerModel( + 'error', + 'Error', + `Error Creating Service`, + errorMessage, + ); + this.growlerService.show(growlerData); + throw resp; + }) + } + + getConfigDataModel(formData, isUpdate) { + const saveModel = new Config(); + const modelProperties = keys(saveModel); + modelProperties.forEach((prop) => { + switch(prop) { + default: + saveModel[prop] = formData[prop]; + } + }); + return saveModel; } } diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.component.html b/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.component.html index c92e86c8..838525f1 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.component.html +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.component.html @@ -41,7 +41,7 @@ [title]="'Add Configurations'" [action]="false" [actionLabel]="svc.attachLabel" - (actionRequested)="svc.attachConfig(svc.selectedConfigId)" + (actionRequested)="attachConfig()" (keyup.enter)="captureConfigEnterEvent($event)" [helpText]="'Configurations are used to define how to intercept and host a service. Ziti Networks come with built-in configuration types (ie. intercept.v1 and host.v1)'" > @@ -90,7 +90,7 @@ Config Name
{{svc.attachLabel}}
@@ -131,13 +131,13 @@
- - - - -
- -
+
@@ -207,7 +207,7 @@ [clickable]="true" [tooltip]="'View Config Data'" (itemRemoved)="svc.removeConfig($event)" - (itemSelected)="svc.previewConfig($event, dynamicForm)" + (itemSelected)="svc.previewConfig($event)" > { this.svc.updatedAddedConfigs(); @@ -150,19 +152,30 @@ export class ServiceFormComponent extends ProjectableForm implements OnInit, OnC } configChanged($event) { - this.svc.configChanged(this.dynamicForm); + if (this.svc.selectedConfigId === 'add-new') { + this.svc.newConfigName = ''; + } + this.svc.configChanged(); } configTypeChanged($event) { - this.svc.configTypeChanged(this.dynamicForm); + this.svc.selectedConfigId = ''; + this.svc.newConfigName = ''; + this.svc.configTypeChanged(); } get showConfigData() { return this.svc.selectedConfigId === 'add-new' || this.svc.selectedConfigId === 'preview'; } + attachConfig() { + this.configEditor.getConfigDataFromForm(); + this.svc.attachConfig(this.svc.selectedConfigId); + } + captureConfigEnterEvent(event) { event.stopPropagation(); + this.configEditor.getConfigDataFromForm(); this.svc.attachConfig(this.svc.selectedConfigId); } @@ -172,6 +185,7 @@ export class ServiceFormComponent extends ProjectableForm implements OnInit, OnC this.save(); break; case 'close': + this.resetForm(); this.closeModal(true); break; case 'toggle-view': @@ -229,6 +243,13 @@ export class ServiceFormComponent extends ProjectableForm implements OnInit, OnC } } + resetForm() { + this.svc.selectedConfigId = ''; + this.svc.selectedConfigTypeId = ''; + this.errors = {}; + this.svc.configErrors = {}; + } + clear(): void { } } diff --git a/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.service.ts b/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.service.ts index 4eb1fb75..b788a0b3 100644 --- a/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.service.ts +++ b/projects/ziti-console-lib/src/lib/features/projectable-forms/service/service-form.service.ts @@ -26,6 +26,7 @@ import moment from 'moment'; import dynamic from "ajv/dist/vocabularies/dynamic"; import {SchemaService} from "../../../services/schema.service"; import {Subscription} from "rxjs"; +import {ConfigEditorComponent} from "../../config-editor/config-editor.component"; export const SERVICE_EXTENSION_SERVICE = new InjectionToken('SERVICE_EXTENSION_SERVICE'); @@ -93,8 +94,9 @@ export class ServiceFormService { 'var(--formSubGroup)' ] - subscription: Subscription = new Subscription();Z + subscription: Subscription = new Subscription(); + configEditor: ConfigEditorComponent; constructor( @Inject(SETTINGS_SERVICE) public settingsService: SettingsService, @Inject(ZITI_DATA_SERVICE) private zitiService: ZitiDataService, @@ -174,14 +176,13 @@ export class ServiceFormService { }); } - previewConfig(configName, dynamicForm) { + previewConfig(configName) { this.showCfgPreviewOption = true; this.configData = this.associatedConfigsMap[configName].data; this.selectedConfigName = this.associatedConfigsMap[configName].name; this.selectedConfigId = 'preview'; this.selectedConfigTypeId = this.associatedConfigsMap[configName].configTypeId; - this.configTypeChanged(dynamicForm, false); - this.updateConfigData(); + this.configTypeChanged(false); this.selectedConfigId = 'preview'; this.newConfigName = this.selectedConfigName; defer(() => { @@ -268,7 +269,7 @@ export class ServiceFormService { }); } - configTypeChanged(dynamicForm?, resetData?) { + configTypeChanged(resetData?) { this.filteredConfigs = this.configs.filter((config) => { return config.configTypeId === this.selectedConfigTypeId; }); @@ -277,18 +278,10 @@ export class ServiceFormService { this.selectedConfigType = configType; } }); - this.selectedConfigId = !isEmpty(this.selectedConfigTypeId) ? 'add-new' : ''; - this.configChanged(dynamicForm, resetData); - } - - routerChanged(event?: any) { - let selectedRouter; - this.routers.forEach((router) => { - if (this.selectedRouterId === router.id) { - selectedRouter = router; - } - }); - this.selectedRouter = selectedRouter; + if (this.selectedConfigId !== 'preview') { + this.selectedConfigId = !isEmpty(this.selectedConfigTypeId) ? 'add-new' : ''; + } + this.configChanged(resetData); } updatedAddedConfigs() { @@ -310,48 +303,11 @@ export class ServiceFormService { if (this.configJsonView) { //this.configSubscriptions.unsubscribe(); } - this.updateConfigData(); - } - - async createForm(dynamicForm) { - this.clearForm(); - if (this.selectedConfigType && dynamicForm) { - if (this.selectedSchema) { - this.renderSchmea(this.selectedSchema, dynamicForm); - } - } - } - - clearForm() { - this.items.forEach((item: any) => { - if (item?.component) item.component.destroy(); - }); - this.items = []; - if (this.subscription) this.subscription.unsubscribe(); - } - - renderSchmea(schema: any, dynamicForm: any) { - if (schema.properties) { - this.items = this.schemaSvc.render(schema, dynamicForm, this.lColorArray, this.bColorArray); - for (let obj of this.items) { - const cRef = obj.component; - cRef.instance.errors = this.errors; - if (cRef?.instance.valueChange) { - const pName: string[] = cRef.instance.parentage; - let parentKey; - if(pName) parentKey = pName.join('.'); - if (parentKey && !this.formData[parentKey]) this.formData[parentKey] = {}; - } - } - } } async attachConfig(addedConfigId) { let configId; if (this.selectedConfigId === 'add-new') { - if (!this.configJsonView) { - this.getConfigDataFromForm(); - } if (!this.validateConfig()) { const growlerData = new GrowlerModel( 'error', @@ -405,6 +361,8 @@ export class ServiceFormService { this.newConfigName = ''; this.selectedConfigTypeId = ''; this.selectedConfigId = ''; + this.configData = {}; + this.configEditor?.getConfigDataFromForm(); return; } } else { @@ -460,76 +418,22 @@ export class ServiceFormService { if (this.selectedConfigId === 'preview' && nameToRemove === this.selectedConfigName) { this.selectedConfigId = ''; this.selectedConfigTypeId = ''; - this.updateConfigData(); } } } - getConfigDataFromForm() { - const data = {}; - this.addItemsToConfig(this.items, data); - this.configData = data; - this.hideConfigJSON = false; - } - - addItemsToConfig(items, data: any = {}, parentType = 'object') { - items.forEach((item) => { - if (item.type === 'array') { - if (item.addedItems) { - data[item.key] = item.addedItems; - } else { - data[item.key] = []; - } - } else if (item.type === 'object') { - const val = this.addItemsToConfig(item.items, {}, item.type); - let hasDefinition = false; - keys(val).forEach((key) => { - if (isBoolean(val[key]) || (!isEmpty(val[key]) && !isNil(val[key]))) { - hasDefinition = true; - } - }); - data[item.key] = hasDefinition ? val : undefined; - } else { - let props = []; - if (item?.component?.instance?.getProperties) { - props = item?.component?.instance?.getProperties(); - } else if (item?.component?.instance) { - props = [{key: item.key, value: item.component.instance.fieldValue}]; - } - props.forEach((prop) => { - data[prop.key] = prop.value; - }); - } - }); - return data; - } - - validateConfigItems(items, parentType = 'object') { - let isValid = true; - items.forEach((item) => { - if (item.type === 'object') { - if (!this.validateConfigItems(item.items, item.type)) { - isValid = false; - } - } else if (item?.component?.instance?.isValid) { - if (!item?.component?.instance?.isValid()) { - isValid = false; - } - } - }); - return isValid; - } - - async configChanged(dynamicForm, resetData = true) { + async configChanged(resetData = true) { let selectedConfig: any = {}; this.configData = resetData ? undefined : this.configData; let data; let attachLabel = 'Attach to Service'; - if (this.selectedConfigId === 'add-new') { + if (this.selectedConfigId === 'preview') { + this.selectedSchema = await this.zitiService.schema(this.selectedConfigType.schema); + } else if (this.selectedConfigId === 'add-new') { data = {}; this.selectedSchema = await this.zitiService.schema(this.selectedConfigType.schema); attachLabel = 'Create and Attach'; - this.createForm(dynamicForm); + //this.createForm(dynamicForm); this.saveDisabled = true; } else if (this.selectedConfigId) { this.filteredConfigs.forEach((config) => { @@ -542,98 +446,17 @@ export class ServiceFormService { } else { this.saveDisabled = false; } - if (!this.configData) { - this.configData = data; - } else { - defer(() => { - this.configData = cloneDeep(data); - }); - } - this.updateConfigData(); - this.attachLabel = attachLabel; - } - - updateConfigData() { - if (!this.configJsonView) { - this.updateFormView(this.items, this.configData); - } else { - this.hideConfigJSON = true; - defer(() => { - this.getConfigDataFromForm(); - }); - } - } - - updateFormView(items, data: any = {}) { - items.forEach((item) => { - if (item.items) { - if (item.type === 'array') { - if (item?.component?.instance?.addedItems) { - item.addedItems = data[item.key] || []; - item.component.instance.addedItems = data[item.key] || []; - } - this.updateFormView(item.items, {}); - } else { - this.updateFormView(item.items, data[item.key]); - } - } else if (item?.component?.instance?.setProperties) { - let val; - switch (item.key) { - case 'forwardingconfig': - val = { - protocol: data.protocol, - address: data.address, - port: data.port, - forwardProtocol: data.forwardProtocol, - forwardAddress: data.forwardAddress, - forwardPort: data.forwardPort, - allowedProtocols: data.allowedProtocols, - allowedAddresses: data.allowedAddresses, - allowedPortRanges: data.allowedPortRanges - } - break; - case 'pap': - val = { - protocol: data.protocol, - address: data.address, - port: data.port, - hostname: data.hostname, - } - break; - default: - val = data[item.key]; - break; - } - item?.component?.instance?.setProperties(val); - } else if (item?.component?.setInput) { - item.component.setInput('fieldValue', data[item.key]); - } - }); - return data; - } - - addTerminator() { - let termAdded = false; - this.addedTerminators.forEach((termName) => { - if (this.selectedRouter.name === termName) { - termAdded = true; - } - }); - if (!termAdded) { - const terminatorModel = { - address: this.terminatorProtocol + ':' + this.terminatorHost + ":" + this.terminatorPort, - binding: this.selectedBindingId, - router: this.selectedRouter.id, - service: undefined + if (this.selectedConfigId !== 'preview') { + if (!this.configData) { + this.configData = data; + } else { + defer(() => { + this.configData = cloneDeep(data); + }); } - this.addedTerminatorNames = [...this.addedTerminatorNames, this.selectedRouter.name]; - this.addedTerminators.push(terminatorModel); - this.selectedRouterId = ''; - this.selectedBindingId = ''; - this.terminatorPort = ''; - this.terminatorHost = ''; - this.terminatorProtocol = ''; } + //this.updateConfigData(); + this.attachLabel = attachLabel; } validate() { @@ -646,13 +469,10 @@ export class ServiceFormService { validateConfig() { this.configErrors = {}; + this.configEditor?.validateConfig(); if (isEmpty(this.newConfigName)) { this.configErrors['name'] = true; } - const configItemsValid = this.validateConfigItems(this.items); - if (!configItemsValid) { - this.configErrors['configData'] = true; - } return isEmpty(this.configErrors); } diff --git a/projects/ziti-console-lib/src/lib/models/config.ts b/projects/ziti-console-lib/src/lib/models/config.ts new file mode 100644 index 00000000..afcc64e3 --- /dev/null +++ b/projects/ziti-console-lib/src/lib/models/config.ts @@ -0,0 +1,5 @@ +export class Config { + name = ''; + data = {}; + configTypeId = ''; +}; \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.html b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.html index 860f4216..9bfb94f7 100644 --- a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.html +++ b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.html @@ -1,6 +1,7 @@
- - - + + + +
diff --git a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.scss b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.scss index 7c5b8d6d..db459e7c 100644 --- a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.scss +++ b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.scss @@ -1,11 +1,3 @@ -:host { - display: flex; - flex: 1 1 auto; - width: 100%; - height: 100%; - position: relative; -} - .configurations { width: 100%; height: 100%; diff --git a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.ts b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.ts index 9ce29985..f3ebbeb4 100644 --- a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.ts +++ b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.component.ts @@ -19,11 +19,10 @@ import {DataTableFilterService} from "../../features/data-table/data-table-filte import {ConfigurationsPageService} from "./configurations-page.service"; import {TabNameService} from "../../services/tab-name.service"; import {ListPageComponent} from "../../shared/list-page-component.class"; -import {CallbackResults} from "../../features/list-page-features/list-page-form/list-page-form.component"; -import {SettingsService} from "../../services/settings.service"; import {ConsoleEventsService} from "../../services/console-events.service"; import {MatDialog} from "@angular/material/dialog"; + @Component({ selector: 'lib-configurations', templateUrl: './configurations-page.component.html', @@ -32,15 +31,11 @@ import {MatDialog} from "@angular/material/dialog"; export class ConfigurationsPageComponent extends ListPageComponent implements OnInit { title = 'Configuration Management' tabs: { url: string, label: string }[] ; - formTitle = ''; - formSubtitle = ''; isLoading: boolean; - showEditForm = false; - showButtons = false; - private schema: any; + formDataChanged = false; constructor( - svc: ConfigurationsPageService, + override svc: ConfigurationsPageService, filterService: DataTableFilterService, private tabNames: TabNameService, consoleEvents: ConsoleEventsService, @@ -52,13 +47,19 @@ export class ConfigurationsPageComponent extends ListPageComponent implements On override ngOnInit() { this.tabs = this.tabNames.getTabs('services'); this.svc.refreshData = this.refreshData; + this.svc.getConfigTypes().then((result) => { + console.log(result); + }); super.ngOnInit(); } headerActionClicked(action: string) { switch (action) { case 'add': - this.openUpdate(); + this.svc.openUpdate(); + break; + case 'edit': + this.svc.openUpdate(action); break; case 'delete': const selectedItems = this.rowData.filter((row) => { @@ -71,37 +72,38 @@ export class ConfigurationsPageComponent extends ListPageComponent implements On } } - itemUpdate() { - - } - - tableAction($event: { action: string; item?: any }) { - - } - - private openUpdate(model?: any) { - if (!model) { - this.formTitle = 'Create Configuration' - this.formSubtitle = 'Add a New Configuration by completing this form'; - } else { - this.formTitle = 'Edit Configuration' - this.formSubtitle = 'Change Configuration details'; + tableAction(event: any) { + switch(event?.action) { + case 'toggleAll': + case 'toggleItem': + this.itemToggled(event.item) + break; + case 'update': + this.svc.openUpdate(event.item); + break; + case 'create': + this.svc.openUpdate(); + break; + case 'delete': + this.deleteItem(event.item) + break; + default: + break; } - this.showEditForm = true; } - viewButtons(state: boolean) { - this.showButtons = state; + deleteItem(item: any) { + this.openBulkDelete([item], 'config'); } - validate = (formData: any): Promise => { - return this.svc.validate(formData, this.schema); - } - - onSchemaChange(schema: any) { - this.schema = schema; + closeModal(event: any) { + this.svc.sideModalOpen = false; + if(event?.refresh) { + this.refreshData(); + } } - closeModal(event: any) { + dataChanged(event) { + this.formDataChanged = event; } } diff --git a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.service.ts b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.service.ts index ed10ec69..206fbc35 100644 --- a/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.service.ts +++ b/projects/ziti-console-lib/src/lib/pages/configurations/configurations-page.service.ts @@ -16,7 +16,7 @@ import {Inject, Injectable} from '@angular/core'; import {DataTableFilterService, FilterObj} from "../../features/data-table/data-table-filter.service"; -import _, {isEmpty} from "lodash"; +import _, {isEmpty, unset} from "lodash"; import moment from "moment"; import {ListPageServiceClass} from "../../shared/list-page-service.class"; import { @@ -27,6 +27,7 @@ import {SchemaService} from "../../services/schema.service"; import {SETTINGS_SERVICE, SettingsService} from "../../services/settings.service"; import {CsvDownloadService} from "../../services/csv-download.service"; import {ExtensionService, SHAREDZ_EXTENSION} from "../../features/extendable/extensions-noop.service"; +import {Service} from "../../models/service"; @Injectable({ providedIn: 'root' @@ -35,7 +36,14 @@ export class ConfigurationsPageService extends ListPageServiceClass { private paging = this.DEFAULT_PAGING; - resourceType = 'configurations'; + resourceType = 'configs'; + selectedConfig: any = {}; + modalType = ''; + + override menuItems = [ + {name: 'Edit', action: 'update'}, + {name: 'Delete', action: 'delete'}, + ] constructor( private schemaSvc: SchemaService, @@ -52,6 +60,13 @@ export class ConfigurationsPageService extends ListPageServiceClass { return moment(row?.data?.createdAt).local().format('M/D/YYYY H:MM A'); } + const typeRenderer = (row) => { + return row?.data?.configType?.name; + }; + + const createdAtHeaderComponentParams = { + filterType: 'DATETIME', + }; return [ { colId: 'name', @@ -59,6 +74,13 @@ export class ConfigurationsPageService extends ListPageServiceClass { headerName: 'Name', headerComponent: TableColumnDefaultComponent, headerComponentParams: this.headerComponentParams, + cellRenderer: this.nameColumnRenderer, + onCellClicked: (data) => { + if (this.hasSelectedText()) { + return; + } + this.openUpdate(data.data); + }, resizable: true, cellClass: 'nf-cell-vert-align tCol', sortable: true, @@ -67,22 +89,23 @@ export class ConfigurationsPageService extends ListPageServiceClass { }, { colId: 'type', - field: 'configType.name', + field: 'type', headerName: 'Type', headerComponent: TableColumnDefaultComponent, - headerComponentParams: this.headerComponentParams, + headerComponentParams: this.typeHeaderComponentParams, + cellRenderer: typeRenderer, resizable: true, cellClass: 'nf-cell-vert-align tCol', sortable: true, filter: true, - sortColumn: this.sort.bind(this) + sortColumn: this.sort.bind(this), }, { colId: 'createdAt', field: 'createdAt', headerName: 'Created At', headerComponent: TableColumnDefaultComponent, - headerComponentParams: this.headerComponentParams, + headerComponentParams: createdAtHeaderComponentParams, valueFormatter: createdAtFormatter, resizable: true, cellClass: 'nf-cell-vert-align tCol', @@ -90,6 +113,43 @@ export class ConfigurationsPageService extends ListPageServiceClass { ]; } + _typeHeaderComponentParams = { + filterType: 'SELECT', + enableSorting: true, + filterOptions: [ + { label: 'ALL', value: '' }, + ], + getFilterOptions: () => { + return this.typeHeaderComponentParams.filterOptions; + } + }; + + get typeHeaderComponentParams() { + return this._typeHeaderComponentParams; + } + + set typeHeaderComponentParams(params) { + this._typeHeaderComponentParams = params; + } + + getConfigTypes() { + const sort = { + ordering: 'asc', + sortBy: 'name' + }; + return super.getTableData('config-types', this.DEFAULT_PAGING, [], sort) + .then((results: any) => { + this.typeHeaderComponentParams.filterOptions = [{ label: 'ALL', value: '' }]; + results.data.forEach((configType: any) => { + this.typeHeaderComponentParams.filterOptions.push({ + label: configType.name, + value: configType.id + }); + }); + return this.typeHeaderComponentParams; + }); + } + getData(filters?: FilterObj[], sort?: any) { // we can customize filters or sorting here before moving on... return super.getTableData('configs', this.paging, filters, sort) @@ -174,33 +234,22 @@ export class ConfigurationsPageService extends ListPageServiceClass { return results; } - private addActionsPerRow(results: any) { + private addActionsPerRow(results: any): any[] { return results.data.map((row) => { - row.actionList = ['update', 'override', 'delete']; - if (row?.enrollment?.ott) { - if (row?.enrollment?.ott?.expiresAt) { - const difference = moment(row?.enrollment?.ott?.expiresAt).diff(moment(new Date())); - if (difference > 0) { - row.actionList.push('download-enrollment'); - row.actionList.push('qr-code'); - } - } else { - row.actionList.push('download-enrollment'); - row.actionList.push('qr-code'); - } - } else if (row?.enrollment?.updb) { - if (row?.enrollment?.updb?.expiresAt != null) { - const difference = moment(row?.enrollment?.updb?.expiresAt).diff(moment(new Date())); - if (difference > 0) { - row.actionList.push('download-enrollment'); - row.actionList.push('qr-code'); - } - } else { - row.actionList.push('download-enrollment'); - row.actionList.push('qr-code'); - } - } + row.actionList = ['update', 'delete']; return row; }); } + + public openUpdate(item?: any) { + this.modalType = 'config'; + if (item) { + this.selectedConfig = item; + this.selectedConfig.badges = []; + unset(this.selectedConfig, '_links'); + } else { + this.selectedConfig = new Service(); + } + this.sideModalOpen = true; + } } diff --git a/projects/ziti-console-lib/src/lib/pages/services/services-page.component.ts b/projects/ziti-console-lib/src/lib/pages/services/services-page.component.ts index 0e913bc5..ccf47e09 100644 --- a/projects/ziti-console-lib/src/lib/pages/services/services-page.component.ts +++ b/projects/ziti-console-lib/src/lib/pages/services/services-page.component.ts @@ -107,11 +107,8 @@ export class ServicesPageComponent extends ListPageComponent implements OnInit, deleteItem(item: any) { this.openBulkDelete([item], 'service'); - console.log('test'); } - - getServiceRoleAttributes() { this.svc.getServiceRoleAttributes().then((result: any) => { this.serviceRoleAttributes = result.data; diff --git a/projects/ziti-console-lib/src/lib/pages/services/services-page.service.ts b/projects/ziti-console-lib/src/lib/pages/services/services-page.service.ts index 17aebd2c..a2355682 100644 --- a/projects/ziti-console-lib/src/lib/pages/services/services-page.service.ts +++ b/projects/ziti-console-lib/src/lib/pages/services/services-page.service.ts @@ -83,12 +83,6 @@ export class ServicesPageService extends ListPageServiceClass { } initTableColumns(): any { - const nameRenderer = (row) => { - return `
- ${row?.data?.name} -
` - } - const rolesRenderer = (row) => { let roles = ''; row?.data?.roleAttributes?.forEach((attr) => { @@ -116,7 +110,7 @@ export class ServicesPageService extends ListPageServiceClass { this.openUpdate(data.data); }, resizable: true, - cellRenderer: nameRenderer, + cellRenderer: this.nameColumnRenderer, cellClass: 'nf-cell-vert-align tCol', sortable: true, filter: true, @@ -233,12 +227,6 @@ export class ServicesPageService extends ListPageServiceClass { if (item) { this.selectedService = item; this.selectedService.badges = []; - // TODO: implement when metrics and dialog features are available - /*this.selectedService.moreActions = [ - {name: 'open-metrics', label: 'Open Metrics'}, - {name: 'dial-logs', label: 'Dial Logs'}, - {name: 'dial-logs', label: 'View Events'}, - ];*/ unset(this.selectedService, '_links'); } else { this.selectedService = new Service(); diff --git a/projects/ziti-console-lib/src/lib/services/schema.service.ts b/projects/ziti-console-lib/src/lib/services/schema.service.ts index f9481dc1..9980d36c 100644 --- a/projects/ziti-console-lib/src/lib/services/schema.service.ts +++ b/projects/ziti-console-lib/src/lib/services/schema.service.ts @@ -642,6 +642,8 @@ export class SchemaService { } else if (subItem?.component?.instance?.fieldValue) { itemData[subItem.key] = subItem.component.instance.fieldValue; subItem.component.instance.fieldValue = undefined; + } else if (subItem.items) { + itemData[subItem.key] = this.addItemData(subItem); } } }); diff --git a/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts b/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts index 047daad4..8f02935f 100644 --- a/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts +++ b/projects/ziti-console-lib/src/lib/services/ziti-controller-data.service.ts @@ -95,7 +95,7 @@ export class ZitiControllerDataService extends ZitiDataService { const apiVersions = this.settingsService.apiVersions || {}; const prefix = apiVersions["edge-management"]?.v1?.path || '/edge/management/v1'; const url = this.settingsService.settings.selectedEdgeController; - const urlFilter = this.getUrlFilter(paging); + const urlFilter = this.getUrlFilter(paging, filters); const serviceUrl = url + prefix + "/" + type + urlFilter; return firstValueFrom(this.httpClient.get(serviceUrl,{}).pipe( @@ -105,18 +105,6 @@ export class ZitiControllerDataService extends ZitiDataService { throw({error: error}); }), map((results: any) => { - if(filters.length > 0) { - filters.forEach((filter:FilterObj) => { - let newData: any[] = []; - if(filter.columnId !== 'name' && !isEmpty(filter.value )) { - results.data.forEach(row => { - if(get(row, filter.columnId)?.indexOf(filter.value) >= 0) - newData.push(row); - }) - results.data = newData; - } - }); - } return results; }) ) @@ -238,25 +226,43 @@ export class ZitiControllerDataService extends ZitiDataService { ); } - private getUrlFilter(paging: any) { + private getUrlFilter(paging, filters: any[]) { let urlFilter = ''; let toSearchOn = "name"; - let noSearch = false; + let noSearch = filters?.length <= 0; if (paging && paging.sort != null) { if (paging.searchOn) toSearchOn = paging.searchOn; if (paging.noSearch) noSearch = true; if (!paging.filter) paging.filter = ""; paging.filter = paging.filter.split('#').join(''); - if (noSearch) { - if (paging.page !== -1) urlFilter = "?limit=" + paging.total + "&offset=" + ((paging.page - 1) * paging.total) + "&sort=" + paging.sort + " " + paging.order; + } + filters.forEach((filter, index) => { + let filterVal = ''; + switch (filter.type) { + case 'TEXTINPUT': + filterVal = `${filter.columnId} contains "${filter.value}"`; + break; + case 'SELECT': + case 'COMBO': + filterVal = `${filter.columnId} = "${filter.value}"`; + break; + case 'DATETIME': + filterVal = `${filter.columnId} >= datetime(${filter.value[0]}) and ${filter.columnId} <= datetime(${filter.value[1]})`; + break; + default: + filterVal = `${filter.columnId} contains "${filter.value}"`; + break; + } + if (index <= 0) { + urlFilter = `?filter= ${filterVal}`; } else { - if (paging.page !== -1) urlFilter = "?filter=(" + toSearchOn + " contains \"" + paging.filter + "\")&limit=" + paging.total + "&offset=" + ((paging.page - 1) * paging.total) + "&sort=" + paging.sort + " " + paging.order; - if (paging.params) { - for (const key in paging.params) { - urlFilter += ((urlFilter.length === 0) ? "?" : "&") + key + "=" + paging.params[key]; - } - } + urlFilter += ` and ${filterVal}` } + }); + if (noSearch) { + if (paging.page !== -1) urlFilter = "?limit=" + paging.total + "&offset=" + ((paging.page - 1) * paging.total) + "&sort=" + paging.sort + " " + paging.order; + } else { + urlFilter += `&limit=${paging.total}&offset=${((paging.page - 1) * paging.total)}&sort=${paging.sort} ${paging.order}` } return urlFilter; } diff --git a/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss b/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss index d8674677..9e175c60 100644 --- a/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss +++ b/projects/ziti-console-lib/src/lib/shared-assets/styles/global.scss @@ -3,6 +3,7 @@ body { overflow: hidden; } +select, input { &.error { border-color: var(--red); @@ -199,7 +200,8 @@ input { } .form-field-extended-fields { - padding: 15px; + padding: var(--paddingXL); + padding-top: var(--paddingSmall); background-color: var(--formSubGroup); border-radius: 7px; &[hidden] { @@ -751,4 +753,132 @@ lib-tag-selector { -webkit-transform: rotate(360deg); transform: rotate(360deg); } +} + +.form-header-toggle-container { + display: flex; + flex-direction: row; + align-items: center; + + .form-header-toggle { + display: flex; + flex-direction: row; + width: 30px; + height: 18px; + border-radius: 10px; + border-color: var(--inputBorder); + border-style: solid; + border-width: 2px; + margin-left: 5px; + margin-right: 5px; + cursor: pointer; + position: relative; + margin-right: 5px; + + &:hover { + filter: brightness(90%); + } + + &:active { + transform: translateY(1px); + filter: brightness(80%); + } + + .form-toggle-switch { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + height: 100%; + margin-left: 3px; + position: absolute; + transition: 0.5s; + left: 0; + + &.toggle-right { + left: 10px; + + .form-toggle-indicator { + border-color: var(--red); + } + } + + .form-toggle-indicator { + height: 10px; + border-color: var(--green); + border-style: solid; + border-left-width: 1px; + border-right-width: 1px; + position: relative; + margin-left: 2px; + } + } + } + + .toggle-option-text { + font-size: 13px; + font-weight: 600; + color: var(--placeholder); + cursor: pointer; + + &:hover { + filter: brightness(90%); + &.toggle-option-selected { + filter: unset; + } + } + + &.toggle-option-selected { + color: var(--tableText); + cursor: default; + } + } +} + +.config-title-row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + .jsonButton { + color: var(--offWhite); + font-weight: 600; + opacity: 1; + } +} + +.form-field-input-group { + gap: 10px; + display: flex; + flex-direction: column; + + .form-field-title { + color: var(--offWhite); + } +} + +.form-field-extended-fields { + label { + color: var(--white) !important; + margin-bottom: 0; + } + .invalid { + input { + border-color: var(--red); + } + .p-component { + &.p-input-wrapper { + border-color: var(--red); + } + } + } +} + +.projectable-form-main-column { + &.form-group-row { + &[hidden] { + display: none; + } + } } \ No newline at end of file diff --git a/projects/ziti-console-lib/src/lib/shared/list-page-service.class.ts b/projects/ziti-console-lib/src/lib/shared/list-page-service.class.ts index dd2d8836..295f19d2 100644 --- a/projects/ziti-console-lib/src/lib/shared/list-page-service.class.ts +++ b/projects/ziti-console-lib/src/lib/shared/list-page-service.class.ts @@ -39,7 +39,7 @@ export abstract class ListPageServiceClass { filterType: 'TEXTINPUT', enableSorting: true }; - + DEFAULT_PAGING: any = { filter: "", noSearch: false, @@ -63,6 +63,12 @@ export abstract class ListPageServiceClass { ordering: 'asc' }; + nameColumnRenderer = (row) => { + return `
+ ${row?.data?.name} +
` + } + constructor( @Inject(SETTINGS_SERVICE) protected settings: SettingsServiceClass, protected filterService: DataTableFilterService, @@ -114,17 +120,7 @@ export abstract class ListPageServiceClass { paging.sort = sort.sortBy; paging.order = sort.ordering; } - let nonNameFilters: FilterObj[] = []; - if(filters) { - for (let idx = 0; idx < filters.length; idx++) { - if (filters[idx].columnId === 'name' && filters[idx].value) { - paging.noSearch = false; - paging.searchOn = 'name' - paging.filter = filters[idx].value; - } else nonNameFilters.push(filters[idx]); - } - } - return this.dataService.get(resourceType, paging, nonNameFilters); + return this.dataService.get(resourceType, paging, filters); } removeItems(ids: string[]) { diff --git a/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts b/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts index 7be30e48..a3939d71 100644 --- a/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts +++ b/projects/ziti-console-lib/src/lib/ziti-console-lib.module.ts @@ -96,6 +96,7 @@ import { SimpleServiceComponent } from './features/projectable-forms/service/sim import { CardComponent } from './features/card/card.component'; import { CreationSummaryDialogComponent } from './features/creation-summary-dialog/creation-summary-dialog.component'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { ConfigEditorComponent } from './features/config-editor/config-editor.component'; export function playerFactory() { return import(/* webpackChunkName: 'lottie-web' */ 'lottie-web'); @@ -162,7 +163,8 @@ export function playerFactory() { CardListComponent, SimpleServiceComponent, CardComponent, - CreationSummaryDialogComponent + CreationSummaryDialogComponent, + ConfigEditorComponent ], imports: [ CommonModule,