From: Pere Diaz Bou Date: Mon, 6 Mar 2023 19:32:24 +0000 (+0100) Subject: mgr/dashboard: replace ajsf with formly X-Git-Tag: v17.2.7~420^2~4 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=36e32132f1cb26f89cc6825c29194aaf05b5c663;p=ceph.git mgr/dashboard: replace ajsf with formly ajsf json schema library for angular doesn't seem to be actively maintained. Instead, fromly is a well maintained replacement with extra stuff like validators builtin, support for json schemas, custom components, etc... Textareas weren't supported on ajsf, therefore, it made sense to move to this dep instead. Signed-off-by: Pere Diaz Bou Signed-off-by: Nizamudeen A (cherry picked from commit 2c43dd0c16e3cc3b3eada03ed11958a689cc4bcd) (cherry picked from commit 768cfbcfe7d937fc34e8afde68974c04569ba962) --- diff --git a/src/pybind/mgr/dashboard/controllers/_crud.py b/src/pybind/mgr/dashboard/controllers/_crud.py index 0ac299db6e46..cbf87e54be37 100644 --- a/src/pybind/mgr/dashboard/controllers/_crud.py +++ b/src/pybind/mgr/dashboard/controllers/_crud.py @@ -89,16 +89,13 @@ class FormField(NamedTuple): field_type: Any = str default_value: Optional[Any] = None optional: bool = False - html_class: str = '' - label_html_class: str = 'col-form-label' - field_html_class: str = 'col-form-input' def get_type(self): _type = '' if self.field_type == str: _type = 'string' elif self.field_type == int: - _type = 'integer' + _type = 'int' elif self.field_type == bool: _type = 'boolean' else: @@ -108,15 +105,12 @@ class FormField(NamedTuple): class Container: def __init__(self, name: str, key: str, fields: List[Union[FormField, "Container"]], - optional: bool = False, html_class: str = '', label_html_class: str = '', - field_html_class: str = ''): + optional: bool = False, min_items=1): self.name = name self.key = key self.fields = fields self.optional = optional - self.html_class = html_class - self.label_html_class = label_html_class - self.field_html_class = field_html_class + self.min_items = min_items def layout_type(self): raise NotImplementedError @@ -135,39 +129,36 @@ class Container: properties = None # control schema properties alias required = None if self._property_type() == 'array': + control_schema['required'] = [] + control_schema['minItems'] = self.min_items control_schema['items'] = { 'type': 'object', 'properties': {}, 'required': [] } properties = control_schema['items']['properties'] - required = control_schema['items']['required'] + required = control_schema['required'] + control_schema['items']['required'] = required + ui_schemas.append({ - 'type': 'array', 'key': key, - 'htmlClass': self.html_class, - 'fieldHtmlClass': self.field_html_class, - 'labelHtmlClass': self.label_html_class, - 'items': [{ - 'type': 'div', - 'flex-direction': self.layout_type(), - 'displayFlex': True, - 'items': [] - }] + 'templateOptions': { + 'objectTemplateOptions': { + 'layoutType': self.layout_type() + } + }, + 'items': [] }) - items = ui_schemas[-1]['items'][0]['items'] + items = ui_schemas[-1]['items'] else: control_schema['properties'] = {} control_schema['required'] = [] required = control_schema['required'] properties = control_schema['properties'] ui_schemas.append({ - 'type': 'section', - 'flex-direction': self.layout_type(), - 'displayFlex': True, - 'htmlClass': self.html_class, - 'fieldHtmlClass': self.field_html_class, - 'labelHtmlClass': self.label_html_class, + 'templateOptions': { + 'layoutType': self.layout_type() + }, 'key': key, 'items': [] }) @@ -196,13 +187,10 @@ class Container: properties[field.key]['type'] = _type properties[field.key]['title'] = field.name field_ui_schema['key'] = field_key - field_ui_schema['htmlClass'] = field.html_class - field_ui_schema['fieldHtmlClass'] = field.field_html_class - field_ui_schema['labelHtmlClass'] = field.label_html_class items.append(field_ui_schema) elif isinstance(field, Container): container_schema = field.to_dict(key+'.'+field.key if key else field.key) - control_schema['properties'][field.key] = container_schema['control_schema'] + properties[field.key] = container_schema['control_schema'] ui_schemas.extend(container_schema['ui_schema']) if not field.optional: required.append(field.key) @@ -245,44 +233,12 @@ class ArrayHorizontalContainer(Container): class Form: - def __init__(self, path, root_container, action: str = '', - footer_html_class: str = 'card-footer position-absolute pb-0 mt-3', - submit_style: str = 'btn btn-primary', cancel_style: str = ''): + def __init__(self, path, root_container): self.path = path - self.action = action self.root_container = root_container - self.footer_html_class = footer_html_class - self.submit_style = submit_style - self.cancel_style = cancel_style def to_dict(self): - container_schema = self.root_container.to_dict() - - # root container style - container_schema['ui_schema'].append({ - 'type': 'flex', - 'flex-flow': f'{self.root_container.layout_type()} wrap', - 'displayFlex': True, - }) - - footer = { - "type": "flex", - "htmlClass": self.footer_html_class, - "items": [ - { - 'type': 'flex', - 'flex-direction': 'row', - 'displayFlex': True, - 'htmlClass': 'd-flex justify-content-end mb-0', - 'items': [ - {"type": "cancel", "style": self.cancel_style, 'htmlClass': 'mr-2'}, - {"type": "submit", "style": self.submit_style, "title": self.action}, - ] - } - ] - } - container_schema['ui_schema'].append(footer) - return container_schema + return self.root_container.to_dict() class CRUDMeta(SerializableClass): diff --git a/src/pybind/mgr/dashboard/controllers/ceph_users.py b/src/pybind/mgr/dashboard/controllers/ceph_users.py index 3562f33a2271..cf49ee6f5186 100644 --- a/src/pybind/mgr/dashboard/controllers/ceph_users.py +++ b/src/pybind/mgr/dashboard/controllers/ceph_users.py @@ -57,23 +57,20 @@ class CephUserEndpoints: raise DashboardException(msg, code=500) return f"Successfully created user '{user_entity}'" - -create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', - label_html_class='hidden cd-header mt-1', fields=[ - FormField('Entity', 'entity', - field_type=str, html_class='mr-3'), - FormField('Entity Capabilities', - 'cap', field_type=str) - ]) -create_container = VerticalContainer('Create User', 'create_user', - html_class='d-none', fields=[ - FormField('User entity', 'user_entity', - field_type=str), - create_cap_container, - ]) +create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[ + FormField('Entity', 'entity', + field_type=str), + FormField('Entity Capabilities', + 'cap', field_type=str) +], min_items=1) +create_container = VerticalContainer('Create User', 'create_user', fields=[ + FormField('User entity', 'user_entity', + field_type=str), + create_cap_container, +]) create_form = Form(path='/cluster/user/create', - root_container=create_container, action='Create User') + root_container=create_container) @CRUDEndpoint( diff --git a/src/pybind/mgr/dashboard/frontend/package-lock.json b/src/pybind/mgr/dashboard/frontend/package-lock.json index 0546043000d7..96ae1dc6c583 100644 --- a/src/pybind/mgr/dashboard/frontend/package-lock.json +++ b/src/pybind/mgr/dashboard/frontend/package-lock.json @@ -10,44 +10,6 @@ "integrity": "sha512-20Pk2Z98fbPLkECcrZSJszKos/OgtvJJR3NcbVfgCJ6EQjDNzW2P1BKqImOz3tJ952dvO2DWEhcLhQ1Wz1e9ng==", "dev": true }, - "@ajsf/bootstrap4": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@ajsf/bootstrap4/-/bootstrap4-0.7.0.tgz", - "integrity": "sha512-wn6wIQeWknmn/t96XZgihfFq/jjr9GkV9P5dHEU+i9wQbxPNL1MS+x4tLWj9LH3Mx5RiC0Dr4gPgbkDd/bzLxg==", - "requires": { - "@ajsf/core": "~0.7.0", - "lodash-es": "~4.17.21", - "tslib": "^2.0.0" - } - }, - "@ajsf/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@ajsf/core/-/core-0.7.0.tgz", - "integrity": "sha512-mysKftZAxT0bHYoia7LzbSinK7Z55wINS63zeK/rqSs9r2dF01Vxtzlx2ITViiok3TQ0UV+1OYce/piozEf4aw==", - "requires": { - "ajv": "^6.10.0", - "lodash-es": "~4.17.21", - "tslib": "^2.0.0" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } - } - }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -4576,6 +4538,22 @@ "integrity": "sha512-wmgOI5sogAuilwBZJqCHVMjm2uhDxjdSmNLFx7eznwGDa6LjvjuATqCv2dVlftq0Y/5oZFVrg5NpyHt5kfZ8Cg==", "dev": true }, + "@ngx-formly/bootstrap": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@ngx-formly/bootstrap/-/bootstrap-6.1.1.tgz", + "integrity": "sha512-yNzASqUrzvhMndERzoTBCvj1rtsgsmMXiXsqIP7PRJ4AdGtsTZvpxNYZAltdKEgJvc1hS/lDMJdS7IHg2qFN9Q==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngx-formly/core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.1.1.tgz", + "integrity": "sha512-6Fg9TBcXXrnUkHqVlpCQbVE5BWJQBvCitQRngW7kiA/+86rhH5mkL19enULWKq7fEMi54uCVvWsz7l6VOaJhLA==", + "requires": { + "tslib": "^2.0.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11711,7 +11689,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-glob": { "version": "3.2.12", @@ -11745,7 +11724,8 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", @@ -24662,6 +24642,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { "punycode": "^2.1.0" }, diff --git a/src/pybind/mgr/dashboard/frontend/package.json b/src/pybind/mgr/dashboard/frontend/package.json index 6ca0fddfda7e..8ff70bcf15cf 100644 --- a/src/pybind/mgr/dashboard/frontend/package.json +++ b/src/pybind/mgr/dashboard/frontend/package.json @@ -44,8 +44,6 @@ }, "private": true, "dependencies": { - "@ajsf/bootstrap4": "0.7.0", - "@ajsf/core": "0.7.0", "@angular/animations": "13.3.11", "@angular/common": "13.3.11", "@angular/compiler": "13.3.11", @@ -57,6 +55,8 @@ "@angular/router": "13.3.11", "@circlon/angular-tree-component": "10.0.0", "@ng-bootstrap/ng-bootstrap": "12.1.2", + "@ngx-formly/bootstrap": "6.1.1", + "@ngx-formly/core": "6.1.1", "@popperjs/core": "2.10.2", "@swimlane/ngx-datatable": "18.0.0", "@types/file-saver": "2.0.1", diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 6880a1561c1d..badaedd996a3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -37,7 +37,7 @@ import { LoginLayoutComponent } from './core/layouts/login-layout/login-layout.c import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component'; import { ApiDocsComponent } from './core/navigation/api-docs/api-docs.component'; import { ActionLabels, URLVerbs } from './shared/constants/app.constants'; -import { CrudFormComponent } from './shared/datatable/crud-table/crud-form/crud-form.component'; +import { CrudFormComponent } from './shared/forms/crud-form/crud-form.component'; import { CRUDTableComponent } from './shared/datatable/crud-table/crud-table.component'; import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs'; import { AuthGuardService } from './shared/services/auth-guard.service'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.html deleted file mode 100644 index 8d7b21b23824..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.html +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
{{ title }}
- -
- - - -
-
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.scss deleted file mode 100644 index 6d21e4c2d89e..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -@use './src/styles/vendor/variables' as vv; - -::ng-deep json-schema-form { - label.control-label.hidden { - display: none; - } - - .form-group.schema-form-submit p { - display: none; - } - - legend { - font-weight: 100 !important; - } - - .card-footer { - border: 1px solid rgba(0, 0, 0, 0.125); - left: -1px; - width: -webkit-fill-available; - width: -moz-available; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.spec.ts deleted file mode 100644 index 7a6faa7e8ace..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ToastrModule, ToastrService } from 'ngx-toastr'; -import { configureTestBed } from '~/testing/unit-test-helper'; -import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe'; -import { CrudFormComponent } from './crud-form.component'; -import { RouterTestingModule } from '@angular/router/testing'; - -describe('CrudFormComponent', () => { - let component: CrudFormComponent; - let fixture: ComponentFixture; - const toastFakeService = { - error: () => true, - info: () => true, - success: () => true - }; - - configureTestBed({ - imports: [ToastrModule.forRoot(), RouterTestingModule, HttpClientTestingModule], - providers: [ - { provide: ToastrService, useValue: toastFakeService }, - { provide: CdDatePipe, useValue: { transform: (d: any) => d } } - ] - }); - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [CrudFormComponent] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(CrudFormComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.ts deleted file mode 100644 index 4545f2ba8a8a..000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { DataGatewayService } from '~/app/shared/services/data-gateway.service'; -import { BackButtonComponent } from '~/app/shared/components/back-button/back-button.component'; -import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; -import { FinishedTask } from '~/app/shared/models/finished-task'; -import { Location } from '@angular/common'; - -@Component({ - selector: 'cd-crud-form', - templateUrl: './crud-form.component.html', - styleUrls: ['./crud-form.component.scss'] -}) -export class CrudFormComponent implements OnInit { - uiSchema: any; - controlSchema: any; - data: any; - widgets: any = { - cancel: BackButtonComponent - }; - resource: string; - title: string; - - formOptions = { - defautWidgetOptions: { - validationMessages: { - required: 'This field is required' - } - } - }; - constructor( - private dataGatewayService: DataGatewayService, - private activatedRoute: ActivatedRoute, - private taskWrapper: TaskWrapperService, - private location: Location - ) {} - - ngOnInit(): void { - this.activatedRoute.data.subscribe((data: any) => { - this.resource = data.resource; - this.dataGatewayService.list(`ui-${this.resource}`).subscribe((response: any) => { - this.title = response.forms[0].control_schema.title; - this.uiSchema = response.forms[0].ui_schema; - this.controlSchema = response.forms[0].control_schema; - }); - }); - } - - submit(data: any) { - if (data) { - this.taskWrapper - .wrapTaskAroundCall({ - task: new FinishedTask('ceph-user/create', { - user_entity: data.user_entity - }), - call: this.dataGatewayService.create(this.resource, data) - }) - .subscribe({ - complete: () => { - this.location.back(); - } - }); - } - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html index b9b4ae62b34d..d1aed4462d7f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html @@ -25,6 +25,18 @@ [tableActions]="meta.actions"> + + + + + + + + +
{{ column }} {{ expandedRow[column] }}
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts index 1cef20b31e35..5f1a5f99175b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts @@ -1,12 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import { NgxPipeFunctionModule } from 'ngx-pipe-function'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormlyModule } from '@ngx-formly/core'; +import { FormlyBootstrapModule } from '@ngx-formly/bootstrap'; import { ComponentsModule } from '../components/components.module'; import { PipesModule } from '../pipes/pipes.module'; import { CRUDTableComponent } from './crud-table/crud-table.component'; @@ -14,8 +16,10 @@ import { TableActionsComponent } from './table-actions/table-actions.component'; import { TableKeyValueComponent } from './table-key-value/table-key-value.component'; import { TablePaginationComponent } from './table-pagination/table-pagination.component'; import { TableComponent } from './table/table.component'; -import { Bootstrap4FrameworkModule } from '@ajsf/bootstrap4'; -import { CrudFormComponent } from './crud-table/crud-form/crud-form.component'; +import { CrudFormComponent } from '../forms/crud-form/crud-form.component'; +import { FormlyArrayTypeComponent } from '../forms/crud-form/formly-array-type/formly-array-type.component'; +import { FormlyInputTypeComponent } from '../forms/crud-form/formly-input-type/formly-input-type.component'; +import { FormlyObjectTypeComponent } from '../forms/crud-form/formly-object-type/formly-object-type.component'; @NgModule({ imports: [ @@ -28,7 +32,16 @@ import { CrudFormComponent } from './crud-table/crud-form/crud-form.component'; PipesModule, ComponentsModule, RouterModule, - Bootstrap4FrameworkModule + ReactiveFormsModule, + FormlyModule.forRoot({ + types: [ + { name: 'array', component: FormlyArrayTypeComponent }, + { name: 'object', component: FormlyObjectTypeComponent }, + { name: 'input', component: FormlyInputTypeComponent } + ], + validationMessages: [{ name: 'required', message: 'This field is required' }] + }), + FormlyBootstrapModule ], declarations: [ TableComponent, @@ -36,7 +49,10 @@ import { CrudFormComponent } from './crud-table/crud-form/crud-form.component'; TableActionsComponent, CRUDTableComponent, TablePaginationComponent, - CrudFormComponent + CrudFormComponent, + FormlyArrayTypeComponent, + FormlyInputTypeComponent, + FormlyObjectTypeComponent ], exports: [ TableComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html new file mode 100644 index 000000000000..9102b892ff55 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html @@ -0,0 +1,25 @@ +
+
+
{{ formUISchema.title }}
+
+ +
+ +
+ +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss new file mode 100644 index 000000000000..6d21e4c2d89e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss @@ -0,0 +1,22 @@ +@use './src/styles/vendor/variables' as vv; + +::ng-deep json-schema-form { + label.control-label.hidden { + display: none; + } + + .form-group.schema-form-submit p { + display: none; + } + + legend { + font-weight: 100 !important; + } + + .card-footer { + border: 1px solid rgba(0, 0, 0, 0.125); + left: -1px; + width: -webkit-fill-available; + width: -moz-available; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts new file mode 100644 index 000000000000..7a6faa7e8ace --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ToastrModule, ToastrService } from 'ngx-toastr'; +import { configureTestBed } from '~/testing/unit-test-helper'; +import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe'; +import { CrudFormComponent } from './crud-form.component'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('CrudFormComponent', () => { + let component: CrudFormComponent; + let fixture: ComponentFixture; + const toastFakeService = { + error: () => true, + info: () => true, + success: () => true + }; + + configureTestBed({ + imports: [ToastrModule.forRoot(), RouterTestingModule, HttpClientTestingModule], + providers: [ + { provide: ToastrService, useValue: toastFakeService }, + { provide: CdDatePipe, useValue: { transform: (d: any) => d } } + ] + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CrudFormComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CrudFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts new file mode 100644 index 000000000000..3734bc84c499 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { DataGatewayService } from '~/app/shared/services/data-gateway.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { Location } from '@angular/common'; +import { FormGroup } from '@angular/forms'; +import { FormlyFormOptions } from '@ngx-formly/core'; +import { mergeMap } from 'rxjs/operators'; +import { JsonFormUISchema } from './crud-form.model'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'cd-crud-form', + templateUrl: './crud-form.component.html', + styleUrls: ['./crud-form.component.scss'] +}) +export class CrudFormComponent implements OnInit { + model: any = {}; + options: FormlyFormOptions = {}; + resource: string; + form = new FormGroup({}); + formUISchema$: Observable; + + constructor( + private dataGatewayService: DataGatewayService, + private activatedRoute: ActivatedRoute, + private taskWrapper: TaskWrapperService, + private location: Location + ) {} + + ngOnInit(): void { + this.formUISchema$ = this.activatedRoute.data.pipe( + mergeMap((data: any) => { + this.resource = data.resource; + return this.dataGatewayService.form(`ui-${this.resource}`); + }) + ); + } + + submit(data: any) { + if (data) { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('ceph-user/create', { + user_entity: data.user_entity + }), + call: this.dataGatewayService.create(this.resource, data) + }) + .subscribe({ + complete: () => { + this.location.back(); + } + }); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts new file mode 100644 index 000000000000..01574edd8640 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts @@ -0,0 +1,7 @@ +import { FormlyFieldConfig } from '@ngx-formly/core'; + +export interface JsonFormUISchema { + title: string; + controlSchema: FormlyFieldConfig[]; + uiSchema: any; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html new file mode 100644 index 000000000000..71083238d371 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html @@ -0,0 +1,42 @@ +
+ {{ props.label }} +

{{ props.description }}

+ +
+ +
+ + +
+
+
+ +
+ + + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss new file mode 100644 index 000000000000..37d7465c8f02 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss @@ -0,0 +1,3 @@ +.action-btn { + margin-top: 2.4rem; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts new file mode 100644 index 000000000000..58a20e2c391b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; + +import { FormlyArrayTypeComponent } from './formly-array-type.component'; + +@Component({ + template: `
+ +
` +}) +class MockFormComponent { + form = new FormGroup({}); + fields: FormlyFieldConfig[] = [ + { + wrappers: ['input'], + defaultValue: {} + } + ]; +} +describe('FormlyArrayTypeComponent', () => { + let component: MockFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FormlyArrayTypeComponent], + imports: [ + FormlyModule.forRoot({ + types: [{ name: 'array', component: FormlyArrayTypeComponent }] + }) + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MockFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts new file mode 100644 index 000000000000..dcbac7001fe8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts @@ -0,0 +1,34 @@ +/** Copyright 2021 Formly. All Rights Reserved. + Use of this source code is governed by an MIT-style license that + can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */ + +import { Component, OnInit } from '@angular/core'; +import { FieldArrayType } from '@ngx-formly/core'; +import { forEach } from 'lodash'; +import { Icons } from '~/app/shared/enum/icons.enum'; + +@Component({ + selector: 'cd-formly-array-type', + templateUrl: './formly-array-type.component.html', + styleUrls: ['./formly-array-type.component.scss'] +}) +export class FormlyArrayTypeComponent extends FieldArrayType implements OnInit { + icons = Icons; + + ngOnInit(): void { + this.propagateTemplateOptions(); + } + + addWrapper() { + this.add(); + this.propagateTemplateOptions(); + } + + propagateTemplateOptions() { + forEach(this.field.fieldGroup, (field) => { + if (field.type == 'object') { + field.props.templateOptions = this.props.templateOptions.objectTemplateOptions; + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html new file mode 100644 index 000000000000..e090e7d6066a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html @@ -0,0 +1,3 @@ + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts new file mode 100644 index 000000000000..0a36356dc239 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; + +import { FormlyInputTypeComponent } from './formly-input-type.component'; + +@Component({ + template: `
+ +
` +}) +class MockFormComponent { + form = new FormGroup({}); + fields: FormlyFieldConfig[] = [ + { + wrappers: ['input'], + defaultValue: {} + } + ]; +} + +describe('FormlyInputTypeComponent', () => { + let component: MockFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FormlyInputTypeComponent], + imports: [ + FormlyModule.forRoot({ + types: [{ name: 'input', component: FormlyInputTypeComponent }] + }) + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MockFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts new file mode 100644 index 000000000000..d310017248f8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { FieldType, FieldTypeConfig } from '@ngx-formly/core'; + +@Component({ + selector: 'cd-formly-input-type', + templateUrl: './formly-input-type.component.html', + styleUrls: ['./formly-input-type.component.scss'] +}) +export class FormlyInputTypeComponent extends FieldType {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html new file mode 100644 index 000000000000..84ec2ab6750f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html @@ -0,0 +1,17 @@ +
+ {{ props.label }} +

{{ props.description }}

+ +
+ +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts new file mode 100644 index 000000000000..b6c4dff059bd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts @@ -0,0 +1,47 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormlyObjectTypeComponent } from './formly-object-type.component'; +import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core'; +import { Component } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + template: `
+ +
` +}) +class MockFormComponent { + form = new FormGroup({}); + fields: FormlyFieldConfig[] = [ + { + wrappers: ['object'], + defaultValue: {} + } + ]; +} + +describe('FormlyObjectTypeComponent', () => { + let fixture: ComponentFixture; + let mockComponent: MockFormComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FormlyObjectTypeComponent], + imports: [ + FormlyModule.forRoot({ + types: [{ name: 'object', component: FormlyObjectTypeComponent }] + }) + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MockFormComponent); + mockComponent = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(mockComponent).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts new file mode 100644 index 000000000000..3dd741227142 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts @@ -0,0 +1,22 @@ +/** Copyright 2021 Formly. All Rights Reserved. + Use of this source code is governed by an MIT-style license that + can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */ + +import { Component } from '@angular/core'; +import { FieldType } from '@ngx-formly/core'; + +@Component({ + selector: 'cd-formly-object-type', + templateUrl: './formly-object-type.component.html', + styleUrls: ['./formly-object-type.component.scss'] +}) +export class FormlyObjectTypeComponent extends FieldType { + get inputClass(): string { + const layoutType = this.props.templateOptions?.layoutType; + const defaultFlexClasses = 'd-flex justify-content-center align-content-stretch gap-3'; + if (layoutType == 'row') { + return defaultFlexClasses + ' flex-row'; + } + return defaultFlexClasses + ' flex-column'; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts new file mode 100644 index 000000000000..7905a11e11aa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CrudFormAdapterService } from './crud-form-adapter.service'; + +describe('CrudFormAdapterService', () => { + let service: CrudFormAdapterService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CrudFormAdapterService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts new file mode 100644 index 000000000000..f9cd35ffdeeb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { FormlyJsonschema } from '@ngx-formly/core/json-schema'; +import { JsonFormUISchema } from '../forms/crud-form/crud-form.model'; + +@Injectable({ + providedIn: 'root' +}) +export class CrudFormAdapterService { + constructor(private formlyJsonschema: FormlyJsonschema) {} + + processJsonSchemaForm(response: any): JsonFormUISchema { + const title = response.forms[0].control_schema.title; + const uiSchema = response.forms[0].ui_schema; + const cSchema = response.forms[0].control_schema; + let controlSchema = this.formlyJsonschema.toFieldConfig(cSchema).fieldGroup; + for (let i = 0; i < controlSchema.length; i++) { + for (let j = 0; j < uiSchema.length; j++) { + if (controlSchema[i].key == uiSchema[j].key) { + controlSchema[i].className = uiSchema[j].className; + controlSchema[i].props.templateOptions = uiSchema[j].templateOptions; + } + } + } + return { title, uiSchema, controlSchema }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts index 6617fdb314af..f6db42818772 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts @@ -2,6 +2,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { JsonFormUISchema } from '../forms/crud-form/crud-form.model'; +import { CrudFormAdapterService } from './crud-form-adapter.service'; @Injectable({ providedIn: 'root' @@ -9,14 +12,12 @@ import { Observable } from 'rxjs'; export class DataGatewayService { cache: { [keys: string]: Observable } = {}; - constructor(private http: HttpClient) {} + constructor(private http: HttpClient, private crudFormAdapater: CrudFormAdapterService) {} list(dataPath: string): Observable { const cacheable = this.getCacheable(dataPath, 'get'); if (this.cache[cacheable] === undefined) { - const match = dataPath.match(/(?[^@]+)(?:@(?.+))?/); - const url = match.groups.url.split('.').join('/'); - const version = match.groups.version || '1.0'; + const { url, version } = this.getUrlAndVersion(dataPath); this.cache[cacheable] = this.http.get(url, { headers: { Accept: `application/vnd.ceph.api.v${version}+json` } @@ -27,16 +28,38 @@ export class DataGatewayService { } create(dataPath: string, data: any): Observable { - const match = dataPath.match(/(?[^@]+)(?:@(?.+))?/); - const url = match.groups.url.split('.').join('/'); - const version = match.groups.version || '1.0'; + const { url, version } = this.getUrlAndVersion(dataPath); return this.http.post(url, data, { headers: { Accept: `application/vnd.ceph.api.v${version}+json` } }); } + form(dataPath: string): Observable { + const cacheable = this.getCacheable(dataPath, 'get'); + if (this.cache[cacheable] === undefined) { + const { url, version } = this.getUrlAndVersion(dataPath); + + this.cache[cacheable] = this.http.get(url, { + headers: { Accept: `application/vnd.ceph.api.v${version}+json` } + }); + } + return this.cache[cacheable].pipe( + map((response) => { + return this.crudFormAdapater.processJsonSchemaForm(response); + }) + ); + } + getCacheable(dataPath: string, method: string) { return dataPath + method; } + + getUrlAndVersion(dataPath: string) { + const match = dataPath.match(/(?[^@]+)(?:@(?.+))?/); + const url = match.groups.url.split('.').join('/'); + const version = match.groups.version || '1.0'; + + return { url: url, version: version }; + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts index ecc8bd03bfe6..7756bed2d943 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts @@ -1,6 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { Bootstrap4FrameworkModule } from '@ajsf/bootstrap4'; +import { ReactiveFormsModule } from '@angular/forms'; +import { FormlyModule } from '@ngx-formly/core'; +import { FormlyBootstrapModule } from '@ngx-formly/bootstrap'; import { CssHelper } from '~/app/shared/classes/css-helper'; import { ComponentsModule } from './components/components.module'; @@ -10,6 +12,9 @@ import { PipesModule } from './pipes/pipes.module'; import { AuthGuardService } from './services/auth-guard.service'; import { AuthStorageService } from './services/auth-storage.service'; import { FormatterService } from './services/formatter.service'; +import { FormlyArrayTypeComponent } from './forms/crud-form/formly-array-type/formly-array-type.component'; +import { FormlyObjectTypeComponent } from './forms/crud-form/formly-object-type/formly-object-type.component'; +import { FormlyInputTypeComponent } from './forms/crud-form/formly-input-type/formly-input-type.component'; @NgModule({ imports: [ @@ -18,7 +23,17 @@ import { FormatterService } from './services/formatter.service'; ComponentsModule, DataTableModule, DirectivesModule, - Bootstrap4FrameworkModule + + ReactiveFormsModule, + FormlyModule.forRoot({ + types: [ + { name: 'array', component: FormlyArrayTypeComponent }, + { name: 'object', component: FormlyObjectTypeComponent }, + { name: 'input', component: FormlyInputTypeComponent } + ], + validationMessages: [{ name: 'required', message: 'This field is required' }] + }), + FormlyBootstrapModule ], declarations: [], exports: [ComponentsModule, PipesModule, DataTableModule, DirectivesModule], diff --git a/src/pybind/mgr/dashboard/frontend/src/styles.scss b/src/pybind/mgr/dashboard/frontend/src/styles.scss index f2c3052f800a..4fdcb1fe3780 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles.scss @@ -218,11 +218,7 @@ a.btn-light { @extend .badge, .bg-dark; } -json-schema-form { - .help-block { - @extend .invalid-feedback; - } - +formly-form { .ng-touched.ng-invalid { @extend .is-invalid; } @@ -230,4 +226,14 @@ json-schema-form { .ng-touched.ng-valid { @extend .is-valid; } + + .form-label { + @extend .cd-col-form-label; + text-align: start; + width: 50%; + + span[aria-hidden='true'] { + color: $danger; + } + } } diff --git a/src/pybind/mgr/dashboard/tests/test_ceph_users.py b/src/pybind/mgr/dashboard/tests/test_ceph_users.py index 35029b32c653..9e0ee525b5ca 100644 --- a/src/pybind/mgr/dashboard/tests/test_ceph_users.py +++ b/src/pybind/mgr/dashboard/tests/test_ceph_users.py @@ -47,4 +47,6 @@ class CephUsersControllerTestCase(ControllerTestCase): def test_create_form(self): form_dict = create_form.to_dict() schema = {'schema': form_dict['control_schema'], 'layout': form_dict['ui_schema']} - validate(instance={'user_entity': 'foo', 'capabilities': []}, schema=schema['schema']) + validate(instance={'user_entity': 'foo', + 'capabilities': [{"entity": "mgr", "cap": "allow *"}]}, + schema=schema['schema']) diff --git a/src/pybind/mgr/dashboard/tests/test_crud.py b/src/pybind/mgr/dashboard/tests/test_crud.py index 97c5728b388b..a94dfad6254f 100644 --- a/src/pybind/mgr/dashboard/tests/test_crud.py +++ b/src/pybind/mgr/dashboard/tests/test_crud.py @@ -43,20 +43,19 @@ def test_schema(): root_container=VerticalContainer('Create user', key='create_user', fields=[ FormField('User entity', key='user_entity', field_type=str), ArrayHorizontalContainer('Capabilities', key='caps', fields=[ - FormField('left', field_type=str, key='left', - html_class='cd-col-form-input'), + FormField('left', field_type=str, key='left'), FormField('right', key='right', field_type=str) ]), ArrayVerticalContainer('ah', key='ah', fields=[ - FormField('top', key='top', field_type=str, label_html_class='d-none'), + FormField('top', key='top', field_type=str), FormField('bottom', key='bottom', field_type=str) ]), HorizontalContainer('oh', key='oh', fields=[ - FormField('left', key='left', field_type=str, label_html_class='d-none'), + FormField('left', key='left', field_type=str), FormField('right', key='right', field_type=str) ]), VerticalContainer('ov', key='ov', fields=[ - FormField('top', key='top', field_type=str, label_html_class='d-none'), + FormField('top', key='top', field_type=str), FormField('bottom', key='bottom', field_type=bool) ]), ]))