diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-auth.helper.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-auth.helper.ts new file mode 100644 index 0000000000..4f2c119a4c --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-auth.helper.ts @@ -0,0 +1,158 @@ +import { ComponentFactoryResolver, Injector } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; +import { ConnectEndpointData } from '../../../features/endpoints/connect.service'; +import { RowState } from '../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { KUBERNETES_ENDPOINT_TYPE } from '../kubernetes-entity-factory'; +import { EndpointAuthTypeConfig, IAuthForm } from './../../../core/extension/extension-types'; +import { KubeConfigFileCluster, KubeConfigFileUser } from './kube-config.types'; + +/** + * Auth helper tries to figure out the Kubernetes sub-type and auth to use + * based on the kube config file contents + */ +export class KubeConfigAuthHelper { + + authTypes: { [name: string]: EndpointAuthTypeConfig } = {}; + + public subTypes = []; + + constructor() { + const epTypeInfo = entityCatalog.getAllEndpointTypes(false); + const k8s = epTypeInfo.find(entity => entity.type === KUBERNETES_ENDPOINT_TYPE); + if (k8s && k8s.definition) { + const defn = k8s.definition; + + // Collect all of the auth types + defn.authTypes.forEach(at => { + this.authTypes[at.value] = at; + }); + + this.subTypes.push({ id: '', name: 'Generic' }); + + // Collect all of the auth types for the sub-types + defn.subTypes.forEach(st => { + if (st.type !== 'config') { + this.subTypes.push({ id: st.type, name: st.labelShort }); + } + st.authTypes.forEach(at => { + this.authTypes[at.value] = at; + }); + }); + + // Sort the subtypes + this.subTypes = this.subTypes.sort((a, b) => a.name.localeCompare(b.name)); + } + } + + // Try and parse the authentication metadata + public parseAuth(cluster: KubeConfigFileCluster, user: KubeConfigFileUser): RowState { + + // Default subtype is generic Kubernetes + cluster._subType = ''; + + // Certificate authentication first + + // In-file certificate authentication + if (user.user['client-certificate-data'] && user.user['client-key-data']) { + // We are good to go - create the form data + + // Default is generic kubernetes + let subType = ''; + const authType = 'kube-cert-auth'; + if (cluster.cluster.server.indexOf('azmk8s.io') >= 0) { + // Probably Azure + subType = 'aks'; + cluster._subType = 'aks'; + } + + const authData = { + authType, + subType, + values: { + cert: user.user['client-certificate-data'], + certKey: user.user['client-key-data'] + } + }; + user._authData = authData; + return {}; + } + + if (user.user['client-certificate'] || user.user['client-key']) { + cluster._additionalUserInfo = true; + return { + message: 'This endpoint will be registered but not connected (additional information is required)', + info: true + }; + } + + const authProvider = user.user['auth-provider']; + + + if (authProvider && authProvider.config) { + if (authProvider.config['cmd-path'] && authProvider.config['cmd-path'].indexOf('gcloud') !== -1) { + // GKE + cluster._subType = 'gke'; + // Can not connect to GKE - user must do so manually + cluster._additionalUserInfo = true; + return { + message: 'This endpoint will be registered but not connected (additional information is required)', + info: true + }; + } + } + + if ( + cluster.cluster.server.indexOf('eks.amazonaws.com') >= 0 || + (user.user.exec && user.user.exec.command && user.user.exec.command === 'aws-iam-authenticator') + ) { + // Probably EKS + cluster._subType = 'eks'; + cluster._additionalUserInfo = true; + return { + message: 'This endpoint will be registered but not connected (additional information is required)', + info: true + }; + } + + return { message: 'Authentication mechanism is not supported', warning: true }; + } + + // Use the auto component to get the data in the correct format for connecting to the endpoint + public getAuthDataForConnect(resolver: ComponentFactoryResolver, injector: Injector, fb: FormBuilder, user: KubeConfigFileUser) + : ConnectEndpointData | null { + + let data = null; + + // Get the component to us + if (user && user._authData) { + const authType = this.authTypes[user._authData.authType]; + + const factory = resolver.resolveComponentFactory(authType.component); + + const ref = factory.create(injector); + + const form = fb.group({ + authType: authType.value, + systemShared: false, + authValues: fb.group(user._authData.values) + }); + + ref.instance.formGroup = form; + + // Allow the auth form to supply body content if it needs to + const endpointFormInstance = ref.instance as any; + if (endpointFormInstance.getBody && endpointFormInstance.getValues) { + data = { + authType: authType.value, + authVal: endpointFormInstance.getValues(user._authData.values), + systemShared: false, + bodyContent: endpointFormInstance.getBody() + }; + } + ref.destroy(); + } + return data; + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.html new file mode 100644 index 0000000000..b78c35d87c --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.scss new file mode 100644 index 0000000000..05510cdc8c --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.scss @@ -0,0 +1,8 @@ +:host { + display: flex; + flex: 1; +} + +.kubeconfig-import { + flex: 1; +} \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.spec.ts new file mode 100644 index 0000000000..a006e2debc --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../kubernetes.testing.module'; +import { KubeConfigImportComponent } from './kube-config-import.component'; + +describe('KubeConfigImportComponent', () => { + let component: KubeConfigImportComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigImportComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigImportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.ts new file mode 100644 index 0000000000..e5e95dffd5 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-import.component.ts @@ -0,0 +1,300 @@ +import { Component, ComponentFactoryResolver, Injector, OnDestroy } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, first, map, pairwise, startWith, withLatestFrom } from 'rxjs/operators'; + +import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; +import { safeUnsubscribe } from '../../../../core/utils.service'; +import { ITableColumn } from '../../../../shared/components/list/list-table/table.types'; +import { KUBERNETES_ENDPOINT_TYPE } from '../../kubernetes-entity-factory'; +import { KubeConfigAuthHelper } from '../kube-config-auth.helper'; +import { KubeConfigFileCluster, KubeConfigImportAction, KubeImportState } from '../kube-config.types'; +import { RegisterEndpoint } from './../../../../../../store/src/actions/endpoint.actions'; +import { AppState } from './../../../../../../store/src/app-state'; +import { EndpointsEffect } from './../../../../../../store/src/effects/endpoint.effects'; +import { endpointSchemaKey } from './../../../../../../store/src/helpers/entity-factory'; +import { ActionState } from './../../../../../../store/src/reducers/api-request-reducer/types'; +import { selectUpdateInfo } from './../../../../../../store/src/selectors/api.selectors'; +import { STRATOS_ENDPOINT_TYPE } from './../../../../base-entity-schemas'; +import { EndpointsService } from './../../../../core/endpoints.service'; +import { + ConnectEndpointConfig, + ConnectEndpointData, + ConnectEndpointService, +} from './../../../../features/endpoints/connect.service'; +import { + ITableListDataSource, + RowState, +} from './../../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { StepOnNextFunction } from './../../../../shared/components/stepper/step/step.component'; +import { + KubeConfigTableImportStatusComponent, +} from './kube-config-table-import-status/kube-config-table-import-status.component'; + +const REGISTER_ACTION = 'Register endpoint'; +const CONNECT_ACTION = 'Connect endpoint'; + +@Component({ + selector: 'app-kube-config-import', + templateUrl: './kube-config-import.component.html', + styleUrls: ['./kube-config-import.component.scss'] +}) +export class KubeConfigImportComponent implements OnDestroy { + + done = new BehaviorSubject(false); + done$ = this.done.asObservable(); + busy = new BehaviorSubject(false); + busy$ = this.busy.asObservable(); + data = new BehaviorSubject([]); + data$ = this.data.asObservable(); + + public dataSource: ITableListDataSource = { + connect: () => this.data$, + disconnect: () => { }, + // Ensure unique per entry to step (in case user went back step and updated) + trackBy: (index, item) => item.cluster.name + this.iteration, + isTableLoading$: this.data$.pipe(map(data => !(data && data.length > 0))), + getRowState: (row: KubeConfigImportAction): Observable => { + return row ? row.state.asObservable() : observableOf({}); + } + }; + public columns: ITableColumn[] = [ + { + columnId: 'action', headerCell: () => 'Action', + cellDefinition: { + valuePath: 'action' + }, + cellFlex: '1', + }, + { + columnId: 'description', headerCell: () => 'Description', + cellDefinition: { + valuePath: 'description' + }, + cellFlex: '4', + }, + // Right-hand column to show the action progress + { + columnId: 'monitorState', + cellComponent: KubeConfigTableImportStatusComponent, + cellConfig: (row) => row.actionState.asObservable(), + cellFlex: '0 0 24px' + } + ]; + + subs: Subscription[] = []; + applyStarted: boolean; + private iteration = 0; + + private endpointEntityKey = entityCatalog.getEntityKey(STRATOS_ENDPOINT_TYPE, endpointSchemaKey); + private connectService: ConnectEndpointService; + + constructor( + public store: Store, + public resolver: ComponentFactoryResolver, + private injector: Injector, + private fb: FormBuilder, + private endpointsService: EndpointsService, + ) { + } + + // Process the next action in the list + private processAction(actions: KubeConfigImportAction[]) { + if (actions.length === 0) { + // We are done + this.done.next(true); + this.busy.next(false); + return; + } + + // Get the next action + const i = actions.shift(); + if (i.action === REGISTER_ACTION) { + this.doRegister(i, actions); + } else if (i.action === CONNECT_ACTION) { + this.doConnect(i, actions); + } else { + // Do the next action + this.processAction(actions); + } + } + + private doRegister(reg: KubeConfigImportAction, next: KubeConfigImportAction[]) { + const obs$ = this.registerEndpoint(reg.cluster.name, reg.cluster.cluster.server, reg.cluster.cluster['insecure-skip-tls-verify']); + const mainObs$ = this.getUpdatingState(obs$).pipe( + startWith({ busy: true, error: false, completed: false }) + ); + + this.subs.push(mainObs$.subscribe(reg.actionState)); + + const sub = reg.actionState.subscribe(progress => { + // Not sure what the status is used for? + reg.status = progress; + if (progress.error && progress.message) { + // Mark all dependency jobs as skip + next.forEach(action => { + if (action.depends === reg) { + // Mark it as skipped by setting the action to null + action.action = null; + action.state.next({ message: 'Skipping action as endpoint could not be registered', warning: true }); + } + }); + reg.state.next({ message: progress.message, error: true }); + } + if (progress.completed) { + if (!progress.error) { + // If we created okay, then guid is in the message + reg.cluster._guid = progress.message; + } + sub.unsubscribe(); + // Do the next one + this.processAction(next); + } + }); + this.subs.push(sub); + } + + private doConnect(connect: KubeConfigImportAction, next: KubeConfigImportAction[]) { + if (!connect.user) { + return; + } + const helper = new KubeConfigAuthHelper(); + const data = helper.getAuthDataForConnect(this.resolver, this.injector, this.fb, connect.user); + if (data) { + const obs$ = this.connectEndpoint(connect, data); + + // Echo obs$ to the behaviour subject + this.subs.push(obs$.subscribe(connect.actionState)); + + this.subs.push(connect.actionState.pipe(filter(status => status.completed), first()).subscribe(status => { + if (status.error) { + connect.state.next({ message: status.errorMessage || status.message, error: true }); + } + this.processAction(next); + })); + } + } + + ngOnDestroy() { + safeUnsubscribe(...this.subs); + + if (this.connectService) { + this.connectService.destroy(); + } + } + + // Register the endpoint + private registerEndpoint(name: string, url: string, skipSslValidation: boolean) { + const action = new RegisterEndpoint(KUBERNETES_ENDPOINT_TYPE, null, name, url, skipSslValidation, '', '', false); + this.store.dispatch(action); + const update$ = this.store.select( + selectUpdateInfo(this.endpointEntityKey, action.guid(), EndpointsEffect.registeringKey) + ).pipe(filter(update => !!update)); + return update$; + } + + // Connect to an endpoint + private connectEndpoint(action: KubeConfigImportAction, pData: ConnectEndpointData) { + const config: ConnectEndpointConfig = { + name: action.cluster.name, + guid: action.depends.cluster._guid || action.cluster._guid, + type: null, + subType: action.user._authData.subType, + ssoAllowed: false + }; + + if (this.connectService) { + this.connectService.destroy(); + } + this.connectService = new ConnectEndpointService(this.store, this.endpointsService, config); + this.connectService.setData(pData); + this.connectService.submit(); + return this.connectService.getConnectingObservable(); + } + + // Enter the step - process the list of clusters to import + onEnter = (data: KubeConfigFileCluster[]) => { + this.applyStarted = false; + this.iteration += 1; + const imports: KubeConfigImportAction[] = []; + data.forEach(item => { + if (item._selected) { + const register = { + action: REGISTER_ACTION, + description: `Register "${item.name}" with the URL "${item.cluster.server}"`, + cluster: item, + state: new BehaviorSubject({}), + actionState: new BehaviorSubject({}), + }; + // Only include if the endpoint does not already exist + if (!item._guid) { + imports.push(register); + } + if (item._additionalUserInfo) { + return; + } + const user = item._users.find(u => u.name === item._user); + if (user) { + imports.push({ + action: CONNECT_ACTION, + description: `Connect "${item.name}" with the user "${user.name}"`, + cluster: item, + user, + state: new BehaviorSubject({}), + depends: register, + actionState: new BehaviorSubject({}), + }); + } + } + }); + this.data.next(imports); + } + + // Finish - go back to the endpoints view + onNext: StepOnNextFunction = () => { + if (this.applyStarted) { + // this.store.dispatch(new RouterNav({ path: ['endpoints'] })); + return observableOf({ success: true, redirect: true }); + + } else { + this.applyStarted = true; + this.busy.next(true); + this.data$.pipe( + filter((data => data && data.length > 0)), + first() + ).subscribe(imports => { + // Go through the imports and dispatch the actions to perform them in sequence + this.processAction([...imports]); + }) + return observableOf({ success: true, ignoreSuccess: true }); + } + } + + // These two should be somewhere else + private getUpdatingState(actionState$: Observable): Observable { + const completed$ = this.getHasCompletedObservable(actionState$.pipe(map(requestState => requestState.busy))); + return actionState$.pipe( + pairwise(), + withLatestFrom(completed$), + map(([[, requestState], completed]) => { + return { + busy: requestState.busy, + error: requestState.error, + completed, + message: requestState.message, + }; + }) + ); + } + + private getHasCompletedObservable(busy$: Observable) { + return busy$.pipe( + distinctUntilChanged(), + pairwise(), + map(([oldBusy, newBusy]) => oldBusy && !newBusy), + startWith(false), + ); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.html new file mode 100644 index 0000000000..e4b06461a1 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.html @@ -0,0 +1 @@ + diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.spec.ts new file mode 100644 index 0000000000..6d2263d935 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigTableImportStatusComponent } from './kube-config-table-import-status.component'; + +describe('KubeConfigTableImportStatusComponent', () => { + let component: KubeConfigTableImportStatusComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableImportStatusComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableImportStatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.ts new file mode 100644 index 0000000000..08bd785052 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component.ts @@ -0,0 +1,26 @@ +import { Component, Input } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-import-status', + templateUrl: './kube-config-table-import-status.component.html', + styleUrls: ['./kube-config-table-import-status.component.scss'] +}) +export class KubeConfigTableImportStatusComponent extends TableCellCustom { + + public state: Observable; + + constructor() { + super(); + } + + @Input() + set config(element) { + if (!this.state) { + this.state = element(this.row); + } + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.html new file mode 100644 index 0000000000..6611b3a683 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.html @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.spec.ts new file mode 100644 index 0000000000..bfcf2b0024 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../kubernetes.testing.module'; +import { KubeConfigImportComponent } from './kube-config-import/kube-config-import.component'; +import { KubeConfigRegistrationComponent } from './kube-config-registration.component'; +import { KubeConfigSelectionComponent } from './kube-config-selection/kube-config-selection.component'; + +describe('KubeConfigRegistrationComponent', () => { + let component: KubeConfigRegistrationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [ + KubeConfigRegistrationComponent, + KubeConfigSelectionComponent, + KubeConfigImportComponent + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigRegistrationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.ts new file mode 100644 index 0000000000..1708ec7897 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-registration.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-kube-config-registration', + templateUrl: './kube-config-registration.component.html', + styleUrls: ['./kube-config-registration.component.scss'] +}) +export class KubeConfigRegistrationComponent { } diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.html new file mode 100644 index 0000000000..77ce84fc8a --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.html @@ -0,0 +1,17 @@ +
+
+ insert_drive_file +

Select a Kube Config file to import clusters

+
+ + +
+
+
+ + +
+ +
+
\ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.scss new file mode 100644 index 0000000000..df0fad3cd0 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.scss @@ -0,0 +1,35 @@ +:host { + display: flex; + flex: 1; +} + +.kube-config-select { + &__panel { + align-items: center; + display: flex; + flex: 1; + justify-content: center; + } + &__upload { + text-align: center; + } + &__title { + font-size: 24px; + text-align: center; + } + &__icon { + font-size: 96px; + height: 96px; + opacity: .7; + width: 96px; + } + &__table { + flex: 1; + } + &__buttons { + padding-bottom: 12px; + button { + margin-right: 24px; + } + } +} \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.spec.ts new file mode 100644 index 0000000000..cc646004a4 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.spec.ts @@ -0,0 +1,29 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../kubernetes.testing.module'; +import { KubeConfigSelectionComponent } from './kube-config-selection.component'; + +describe('KubeConfigSelectionComponent', () => { + let component: KubeConfigSelectionComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigSelectionComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigSelectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.ts new file mode 100644 index 0000000000..4a89f59baf --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-selection.component.ts @@ -0,0 +1,210 @@ +import { Component, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, combineLatest, Observable, of as observableOf, of } from 'rxjs'; +import { first, map, switchMap } from 'rxjs/operators'; + +import { HideSnackBar, ShowSnackBar } from '../../../../../../store/src/actions/snackBar.actions'; +import { AppState } from '../../../../../../store/src/app-state'; +import { + TableHeaderSelectComponent, +} from '../../../../shared/components/list/list-table/table-header-select/table-header-select.component'; +import { KubeConfigHelper } from '../kube-config.helper'; +import { KubeConfigFileCluster } from '../kube-config.types'; +import { + ITableListDataSource, + RowState, +} from './../../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { ITableColumn } from './../../../../shared/components/list/list-table/table.types'; +import { KubeConfigTableCertComponent } from './kube-config-table-cert/kube-config-table-cert.component'; +import { KubeConfigTableName } from './kube-config-table-name/kube-config-table-name.component'; +import { KubeConfigTableSelectComponent } from './kube-config-table-select/kube-config-table-select.component'; +import { + KubeConfigTableSubTypeSelectComponent, +} from './kube-config-table-sub-type-select/kube-config-table-sub-type-select.component'; +import { KubeConfigTableUserSelectComponent } from './kube-config-table-user-select/kube-config-table-user-select.component'; + +export interface KubeConfigTableListDataSource extends ITableListDataSource { + editRowName: string; +} + +@Component({ + selector: 'app-kube-config-selection', + templateUrl: './kube-config-selection.component.html', + styleUrls: ['./kube-config-selection.component.scss'], + providers: [ + KubeConfigHelper + ], +}) +export class KubeConfigSelectionComponent { + + @Input() applyStarted: boolean; + public dataSource: KubeConfigTableListDataSource = { + connect: () => this.helper.clusters$, + disconnect: () => { }, + trackBy: (index, row) => row.name, + isTableLoading$: observableOf(false), + getRowState: (row: KubeConfigFileCluster, schemaKey: string): Observable => { + return row ? row._state.asObservable() : observableOf({}); + }, + selectAllIndeterminate: false, + selectAllChecked: false, + selectAllFilteredRows: () => { + // Should always go to true from indeterminate + this.dataSource.selectAllChecked = this.dataSource.selectAllIndeterminate ? true : !this.dataSource.selectAllChecked + this.dataSource.selectAllIndeterminate = false; // either all off or all on, cannot be indeterminate + + this.helper.clusters$.pipe( + first(), + switchMap(clusters => combineLatest(clusters.map(cluster => { + if (!cluster._invalid) { + cluster._selected = this.dataSource.selectAllChecked; + return this.helper.checkValidity(cluster).pipe(map(() => cluster)); + } + return of(cluster); + }))), + first(), + ).subscribe(clusters => { + this.checkCanGoNext(clusters); + }) + }, + editRow: null, + editRowName: null, + startEdit: (c: KubeConfigFileCluster) => { + this.dataSource.editRow = c; + }, + saveEdit: () => { + this.dataSource.editRow.name = this.dataSource.editRowName; + this.helper.update(this.dataSource.editRow); + delete this.dataSource.editRowName; + delete this.dataSource.editRow; + }, + cancelEdit: () => { + delete this.dataSource.editRowName; + delete this.dataSource.editRow; + }, + getRowUniqueId: (c: KubeConfigFileCluster) => c ? c._id : null + }; + + public columns: ITableColumn[] = [ + { + columnId: 'select', + headerCellComponent: TableHeaderSelectComponent, + cellComponent: KubeConfigTableSelectComponent, + class: 'table-column-select', + cellFlex: '0 0 48px' + }, + { + columnId: 'name', headerCell: () => 'Name', + cellComponent: KubeConfigTableName, + cellFlex: '3', + class: 'app-table__cell--table-no-v-padding' + }, + { + columnId: 'url', headerCell: () => 'URL', + cellDefinition: { + valuePath: 'cluster.server' + }, + cellFlex: '4', + }, + { + columnId: 'type', headerCell: () => 'Type', + cellFlex: '1', + cellComponent: KubeConfigTableSubTypeSelectComponent + }, + { + columnId: 'user', headerCell: () => 'User', + cellFlex: '4', + cellComponent: KubeConfigTableUserSelectComponent + }, + { + columnId: 'cert', headerCell: () => 'Skip SSL Validation', + cellFlex: '0 0 62px', + class: 'app-table__cell--table-centred', + cellComponent: KubeConfigTableCertComponent + } + ]; + + // Is the import data valid? + valid = new BehaviorSubject(false); + valid$ = this.valid.asObservable(); + + canSetIntermediate = false; + + constructor( + private store: Store, + public helper: KubeConfigHelper + ) { + this.helper.clustersChanged = () => this.clustersChanged() + } + + // Save data for the next step to know the list of clusters to import + onNext = () => this.helper.clusters$.pipe( + first(), + map(clusters => ({ + success: true, + data: clusters + })) + ) + + clustersParse(cluster: string) { + this.store.dispatch(new HideSnackBar()); + this.helper.parse(cluster).pipe(first()).subscribe(errorString => { + if (errorString) { + this.store.dispatch(new ShowSnackBar(`Failed to load Kube Config: ${errorString}`, 'Close')) + } + }) + } + + onEnter = () => { + if (!this.applyStarted) { + return; + } + // Handle back from review step (ensure newly registered endpoints are taken into account) + this.helper.updateAll().pipe(first()).subscribe(() => { }) + } + + // Row changed event - update the next button and selection state + clustersChanged() { + this.helper.clusters$.pipe( + first() + ).subscribe(clusters => { + this.checkCanGoNext(clusters); + + // Check the select all state + let selectedCount = 0; + let totalCount = 0; + clusters.forEach(i => { + if (!i._invalid) { + totalCount++; + selectedCount += i._selected ? 1 : 0; + } + }); + + if (selectedCount === 0 || totalCount === selectedCount) { + this.dataSource.selectAllIndeterminate = false; + this.dataSource.selectAllChecked = (selectedCount !== 0); + } else { + this.dataSource.selectAllIndeterminate = true; + } + }) + + } + + // Can we proceed? + checkCanGoNext(clusters: KubeConfigFileCluster[]) { + let selected = 0; + let okay = 0; + clusters.forEach(i => { + if (i._selected) { + selected++; + if (!i._invalid) { + okay++; + } + } + }); + + // Must be at least one selected and they all must be okay to import + this.valid.next(selected > 0 && selected === okay); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.html new file mode 100644 index 0000000000..5d0a8d6faf --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.spec.ts new file mode 100644 index 0000000000..0b1bdaf8b8 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.spec.ts @@ -0,0 +1,33 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigTableCertComponent } from './kube-config-table-cert.component'; + +describe('KubeConfigTableCertComponent', () => { + let component: KubeConfigTableCertComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableCertComponent], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableCertComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.ts new file mode 100644 index 0000000000..c776a3d0bf --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component.ts @@ -0,0 +1,73 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Component, Input } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { timeout } from 'rxjs/operators'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +type CertResponse = { + Status: number; + Required: boolean; + Error: boolean; + Message: string; +} + +@Component({ + selector: 'app-kube-config-table-cert', + templateUrl: './kube-config-table-cert.component.html', + styleUrls: ['./kube-config-table-cert.component.scss'] +}) +export class KubeConfigTableCertComponent extends TableCellCustom { + + initialValue = new BehaviorSubject<{ + checked: boolean + }>(null) + initialValue$ = this.initialValue.asObservable(); + + private pRow: KubeConfigFileCluster; + @Input() + set row(row: KubeConfigFileCluster) { + if (!this.pRow) { + this.pRow = row; + if (row.cluster['insecure-skip-tls-verify']) { + // User has manually specified default skip option + this.initialValue.next({ + checked: true + }); + } else { + // Manually check if a cert is required, if so tick by default + this.http.get(`/pp/v1/kube/cert?url=${row.cluster.server}`).pipe( + timeout(5000), + ).subscribe( + // Success, no cert required + (res: CertResponse) => this.update(res.Required), + // Failed, check for specific cert required error + (e: HttpErrorResponse) => this.update(false) + ) + } + } + } + get row(): KubeConfigFileCluster { + return this.pRow; + } + + constructor( + private helper: KubeConfigHelper, + private http: HttpClient + ) { + super() + } + + private update(checked: boolean) { + this.initialValue.next({ checked }); + this.valueChanged(checked); + } + + valueChanged(value) { + this.row.cluster['insecure-skip-tls-verify'] = value; + this.helper.update(this.row); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.html new file mode 100644 index 0000000000..39cdc6abec --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.html @@ -0,0 +1,9 @@ +
+ + + + {{row.name}} + +
\ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.scss new file mode 100644 index 0000000000..1d85cadb70 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.scss @@ -0,0 +1,12 @@ +.name { + align-items: center; + display: flex; + + .cell-edit-variable { + flex: 1; + } + + app-table-cell-edit { + margin-bottom: 3px; + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.spec.ts new file mode 100644 index 0000000000..c37211591f --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.spec.ts @@ -0,0 +1,34 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IListDataSource } from '../../../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableName } from './kube-config-table-name.component'; + +describe('KubeConfigTableName', () => { + let component: KubeConfigTableName; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableName] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableName); + component = fixture.componentInstance; + component.dataSource = { + getRowUniqueId: (row) => "" + } as IListDataSource + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.ts new file mode 100644 index 0000000000..576f4c19f2 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-name', + templateUrl: './kube-config-table-name.component.html', + styleUrls: ['./kube-config-table-name.component.scss'] +}) +export class KubeConfigTableName extends TableCellCustom { } diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.html new file mode 100644 index 0000000000..944326f9e6 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.spec.ts new file mode 100644 index 0000000000..3bf72f8139 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableSelectComponent } from './kube-config-table-select.component'; + +describe('KubeConfigTableSelectComponent', () => { + let component: KubeConfigTableSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableSelectComponent], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableSelectComponent); + component = fixture.componentInstance; + component.row = {} as KubeConfigFileCluster; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.ts new file mode 100644 index 0000000000..b13745ed48 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-select', + templateUrl: './kube-config-table-select.component.html', + styleUrls: ['./kube-config-table-select.component.scss'] +}) +export class KubeConfigTableSelectComponent extends TableCellCustom { + + constructor(private helper: KubeConfigHelper) { + super(); + } + changed(v) { + this.row._selected = v.checked; + this.helper.update(this.row); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.html new file mode 100644 index 0000000000..92a8d0e90e --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.html @@ -0,0 +1,3 @@ + + {{ type.name }} + diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.spec.ts new file mode 100644 index 0000000000..c2ce574749 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.spec.ts @@ -0,0 +1,35 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableSubTypeSelectComponent } from './kube-config-table-sub-type-select.component'; + +describe('KubeConfigTableSubTypeSelectComponent', () => { + let component: KubeConfigTableSubTypeSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [KubeConfigTableSubTypeSelectComponent], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableSubTypeSelectComponent); + component = fixture.componentInstance; + component.row = {} as KubeConfigFileCluster; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.ts new file mode 100644 index 0000000000..8d487d1bc6 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from '@angular/core'; + +import { TableCellCustom } from '../../../../../../../core/src/shared/components/list/list.types'; +import { KubeConfigAuthHelper } from '../../kube-config-auth.helper'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-sub-type-select', + templateUrl: './kube-config-table-sub-type-select.component.html', + styleUrls: ['./kube-config-table-sub-type-select.component.scss'] +}) +export class KubeConfigTableSubTypeSelectComponent extends TableCellCustom implements OnInit { + + selected: string; + + subTypes: string[]; + + constructor(private helper: KubeConfigHelper) { + super(); + + this.subTypes = new KubeConfigAuthHelper().subTypes; + } + + ngOnInit() { + this.selected = this.row._subType || ''; + } + + valueChanged(value) { + this.row._subType = value; + this.helper.update(this.row); + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.html b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.html new file mode 100644 index 0000000000..1c1c629a8a --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.html @@ -0,0 +1,9 @@ +
+ + Register Only + {{ user.name }} + +
+
+ No user found, register only +
\ No newline at end of file diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.scss b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.spec.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.spec.ts new file mode 100644 index 0000000000..3ada12009f --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.spec.ts @@ -0,0 +1,39 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { KubernetesBaseTestModules } from '../../../kubernetes.testing.module'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; +import { KubeConfigTableUserSelectComponent } from './kube-config-table-user-select.component'; + +describe('KubeConfigTableUserSelectComponent', () => { + let component: KubeConfigTableUserSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ...KubernetesBaseTestModules + ], + declarations: [ + KubeConfigTableUserSelectComponent, + ], + providers: [ + KubeConfigHelper + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(KubeConfigTableUserSelectComponent); + component = fixture.componentInstance; + component.row = { + _users: [] + } as KubeConfigFileCluster; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.ts new file mode 100644 index 0000000000..f39471a321 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; + +import { TableCellCustom } from '../../../../../shared/components/list/list.types'; +import { KubeConfigHelper } from '../../kube-config.helper'; +import { KubeConfigFileCluster } from '../../kube-config.types'; + +@Component({ + selector: 'app-kube-config-table-user-select', + templateUrl: './kube-config-table-user-select.component.html', + styleUrls: ['./kube-config-table-user-select.component.scss'] +}) +export class KubeConfigTableUserSelectComponent extends TableCellCustom implements OnInit { + + hasUser = false; + selected: string; + + constructor(private helper: KubeConfigHelper) { + super(); + } + + ngOnInit() { + this.selected = this.row._user || ''; + this.hasUser = this.row._users.length > 0; + } + + valueChanged(value) { + this.row._user = value; + this.helper.update(this.row); + } + +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.helper.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.helper.ts new file mode 100644 index 0000000000..645505ea91 --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.helper.ts @@ -0,0 +1,201 @@ +import { Injectable } from '@angular/core'; +import * as yaml from 'js-yaml'; +import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; +import { filter, first, map, tap } from 'rxjs/operators'; + +import { EndpointModel } from '../../../../../store/src/types/endpoint.types'; +import { EndpointsService } from '../../../core/endpoints.service'; +import { createGuid } from '../../../core/utils.service'; +import { getFullEndpointApiUrl } from '../../../features/endpoints/endpoint-helpers'; +import { RowState } from '../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { KubeConfigAuthHelper } from './kube-config-auth.helper'; +import { KubeConfigFile, KubeConfigFileCluster } from './kube-config.types'; + +/** + * Helper to parse the kubeconfig and transform it into data + * that we can display in a table for selection + * + * Main issue is we only support one credential per endpoint, so need to format the data + * to offer the user ability to select which user to import + */ +@Injectable() +export class KubeConfigHelper { + + authHelper = new KubeConfigAuthHelper(); + + clusters = new BehaviorSubject(null) + clusters$ = this.clusters.asObservable().pipe( + filter(clusters => !!clusters) + ); + + constructor( + public endpointsService: EndpointsService, + ) { + } + + public clustersChanged: () => void; + public update = (cluster: KubeConfigFileCluster) => { + this.checkValidity(cluster).subscribe(() => this.clustersChanged()); + } + + public updateAll(): Observable { + return this.clusters$.pipe( + tap(clusters => clusters.forEach(cluster => this.update(cluster))), + ) + } + + public parse(config: string): Observable { + let doc: KubeConfigFile; + + const clusters: { [name: string]: KubeConfigFileCluster } = {}; + + try { + doc = yaml.safeLoad(config); + } catch (e) { + return of(`${e}`); + } + + // Need contexts, users and clusters + if (!doc || !doc.contexts || !doc.users || !doc.clusters) { + return of(`Configuration must have contexts, users and clusters`); + } + + // Go through all of the contexts and find the clusters + doc.contexts.forEach(ctx => { + const cluster = doc.clusters.find(item => item.name === ctx.context.cluster); + if (cluster) { + // Found the cluster + if (!clusters[cluster.name]) { + const clstr = { + ...cluster, + _users: [] + }; + clusters[cluster.name] = clstr; + clstr._state = new BehaviorSubject({}); + } + + // Get the user + const user = doc.users.find(item => item.name === ctx.context.user); + if (user) { + // Check we don't already have this user (remove duplicates) + const users = clusters[cluster.name]._users; + if (users.findIndex(usr => usr.name === user.name) === -1) { + clusters[cluster.name]._users.push(user); + if (ctx.name === doc['current-context']) { + // Auto-select this cluster/user if it is the current context + clusters[cluster.name]._user = user.name; + clusters[cluster.name]._selected = true; + } + } + } + } + }); + + // Go through all clusters, auto-select the user where this is only 1 and check validity + const clustersArray = Object.values(clusters); + clustersArray.forEach(cluster => { + if (cluster._users.length >= 1) { + cluster._user = cluster._users[0].name; + } + cluster._id = createGuid(); + }); + + // Check validity + return combineLatest( + clustersArray.map(cluster => this.checkValidity(cluster)) + ).pipe( + map(() => { + // Notify cluster changes + this.clustersChanged(); + this.clusters.next(Object.values(clusters)); + return ''; + }) + ); + } + + + // Check the validity of a cluster for import + public checkValidity(cluster: KubeConfigFileCluster): Observable { + // Check endpoint name + return combineLatest([ + this.endpointsService.endpoints$, + this.clusters.asObservable() // Might be called before we've loaded clusters, so used the non-filtered one + ]).pipe( + first(), + map(([eps, clusters]) => this.validate(Object.values(eps), cluster, clusters)) + ); + } + + private validate(endpoints: EndpointModel[], cluster: KubeConfigFileCluster, clusters: KubeConfigFileCluster[]) { + cluster._invalid = false; + let reset = true; + + const found = endpoints.find(item => item.name === cluster.name); + if (found) { + // If the URL is the same, then we will just connect to the existing endpoint + if (getFullEndpointApiUrl(found) === cluster.cluster.server && !!cluster._user) { + cluster._guid = found.guid; + cluster._state.next({ + message: 'This endpoint will be connected and not registered (endpoint is already registered)', + info: true + }); + reset = false; + } else { + // An endpoint with the same name (but different URL) already exists + cluster._invalid = true; + cluster._state.next({ message: 'An endpoint with this name already exists', warning: true }); + } + } else { + // Check endpoint url is not registered with a different name + if (endpoints.find(item => getFullEndpointApiUrl(item) === cluster.cluster.server)) { + cluster._invalid = true; + cluster._state.next({ message: 'An endpoint with this URL already exists', warning: true }); + } + } + + // Check the connection details + if (!cluster._invalid && cluster._user) { + const user = cluster._users.find(item => item.name === cluster._user); + if (user) { + const newState = this.authHelper.parseAuth(cluster, user); + if (!!newState && !!newState.message) { + reset = false; + cluster._invalid = newState.error || newState.warning + cluster._state.next(newState); + } + } + } + + // Register only (_additionalUserInfo.. specific to text warning) is true + // Connect only (endpoint exists) is true + // Show special warning + if (cluster._additionalUserInfo && cluster._guid) { + cluster._invalid = true; + reset = true; + cluster._state.next({ + message: 'This endpoint will not be registered or connected (endpoint is already registered, additional information required to connect)', + warning: true + }); + } + + if (clusters && !!clusters.find(candidate => candidate.name === cluster.name && candidate._id !== cluster._id)) { + cluster._invalid = true; + cluster._state.next({ message: 'An endpoint with this name already exists in the config file', warning: true }); + } + + if (!cluster.name) { + cluster._invalid = true; + cluster._state.next({ message: 'Cluster must have name', warning: true }); + } + + // Cluster is valid, so clear any warning or error message + if (!cluster._invalid && reset) { + cluster._state.next({}); + } + + // Ensure invalid rows aren't selected (user cannot unselect invalid rows) + if (cluster._invalid) { + cluster._selected = false; + } + } +} diff --git a/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.types.ts b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.types.ts new file mode 100644 index 0000000000..f2d8b7abde --- /dev/null +++ b/custom-src/frontend/app/custom/kubernetes/kube-config-registration/kube-config.types.ts @@ -0,0 +1,99 @@ +import { Observable, Subject } from 'rxjs'; + +import { EndpointAuthTypeConfig } from '../../../core/extension/extension-types'; +import { RowState } from '../../../shared/components/list/data-sources-controllers/list-data-source-types'; +import { ActionStatus } from './../../../../../store/src/reducers/api-request-reducer/types'; + +// Types for a Kubernetes Configuration file + +export interface KubeConfigFileCluster { + name: string; + cluster: { + 'certificate-authority': string; + 'certificate-authority-data': string; + 'insecure-skip-tls-verify': boolean; + server: string; + }; + // Selected user to import + _user: string; + _users: KubeConfigFileUser[]; + // _onUpdate: (row) => {}; + // Is the cluster selected for import? + _selected: boolean; + // Is this cluster invalid? i.e. requires more information + _invalid: boolean; + // row state + _state: Subject; + // status of import + _status: string; + // guid of the existing endpoint for this cluster + _guid: string; + // subtype + _subType?: string; + // additional info is required in order to connect, hints at register only, though is specific due to warning message + _additionalUserInfo: boolean; + // unique identifier + _id: string; +} + +export interface KubeConfigFileUser { + name: string; + user: KubeConfigFileUserDetail; + _authData: KubeConfigImportAuthConfig; +} + +export interface KubeConfigFileUserDetail { + 'client-certificate'?: string; + 'client-key'?: string; + 'client-certificate-data'?: string; + 'client-key-data'?: string; + token?: string; + exec?: any +} + +export interface KubeConfigFileContext { + name: string; + context: { + cluster: string; + user: string; + }; +} + +export interface KubeConfigFile { + apiVersion: string; + clusters: KubeConfigFileCluster[]; + contexts: KubeConfigFileContext[]; + 'current-context': string; + kind: string; + users: KubeConfigFileUser[]; +} + +export interface KubeConfigImportAction { + action: string; + description: string; + cluster: KubeConfigFileCluster; + user?: KubeConfigFileUser; + status?: ActionStatus; + state: Subject; + actionState$?: Observable; + actionState: Subject; + depends?: KubeConfigImportAction; +} + +export interface KubeImportState { + busy: boolean; + error: boolean; + completed: boolean; + message: string; +} + +export interface EndpointConfig { + type: string; + authTypes: EndpointAuthTypeConfig[]; +} + +export interface KubeConfigImportAuthConfig { + subType: string; + authType: string; + values: { [key: string]: string }; +} diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts index eac9f993c0..15e0ea101b 100644 --- a/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts +++ b/custom-src/frontend/app/custom/kubernetes/kubernetes-entity-generator.ts @@ -20,6 +20,7 @@ import { KubernetesConfigAuthFormComponent, } from './auth-forms/kubernetes-config-auth-form/kubernetes-config-auth-form.component'; import { KubernetesGKEAuthFormComponent } from './auth-forms/kubernetes-gke-auth-form/kubernetes-gke-auth-form.component'; +import { KubeConfigRegistrationComponent } from './kube-config-registration/kube-config-registration.component'; import { kubeEntityCatalog } from './kubernetes-entity-catalog'; import { KUBERNETES_ENDPOINT_TYPE, @@ -129,31 +130,44 @@ export function generateKubernetesEntities(): StratosBaseCatalogEntity[] { urlValidation: undefined, authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CERT_AUTH], kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], renderPriority: 4, - subTypes: [{ - type: 'caasp', - label: 'SUSE CaaS Platform', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], - logoUrl: '/core/assets/custom/caasp.png', - renderPriority: 5 - }, { - type: 'aks', - label: 'Azure AKS', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG_AZ]], - logoUrl: '/core/assets/custom/aks.svg', - renderPriority: 6 - }, { - type: 'eks', - label: 'Amazon EKS', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.AWS_IAM]], - logoUrl: '/core/assets/custom/eks.svg', - renderPriority: 6 - }, { - type: 'gke', - label: 'Google Kubernetes Engine', - authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.GKE]], - logoUrl: '/core/assets/custom/gke.svg', - renderPriority: 6 - }], + subTypes: [ + { + type: 'config', + label: 'Import Kubeconfig', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], + logoUrl: '/core/assets/custom/kube_import.png', + renderPriority: 3, + registrationComponent: KubeConfigRegistrationComponent, + }, + { + type: 'caasp', + label: 'SUSE CaaS Platform', + labelShort: 'CaaSP', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG]], + logoUrl: '/core/assets/custom/caasp.png', + renderPriority: 5, + }, { + type: 'aks', + label: 'Azure AKS', + labelShort: 'AKS', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.CONFIG_AZ]], + logoUrl: '/core/assets/custom/aks.svg', + renderPriority: 6 + }, { + type: 'eks', + label: 'Amazon EKS', + labelShort: 'EKS', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.AWS_IAM]], + logoUrl: '/core/assets/custom/eks.svg', + renderPriority: 6 + }, { + type: 'gke', + label: 'Google Kubernetes Engine', + labelShort: 'GKE', + authTypes: [kubeAuthTypeMap[KubeEndpointAuthTypes.GKE]], + logoUrl: '/core/assets/custom/gke.svg', + renderPriority: 6 + }], }; return [ generateEndpointEntity(endpointDefinition), diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts index ed05e9af79..0c2111ed22 100644 --- a/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts +++ b/custom-src/frontend/app/custom/kubernetes/kubernetes.module.ts @@ -1,4 +1,3 @@ -/* tslint:disable:max-line-length */ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { NgxChartsModule } from '@swimlane/ngx-charts'; @@ -93,6 +92,7 @@ import { KubernetesNodesTabComponent } from './tabs/kubernetes-nodes-tab/kuberne import { KubernetesPodsTabComponent } from './tabs/kubernetes-pods-tab/kubernetes-pods-tab.component'; import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kubernetes-summary.component'; +/* tslint:disable:max-line-length */ /* tslint:enable */ @@ -146,7 +146,7 @@ import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kub KubernetesResourceViewerComponent, KubeServiceCardComponent, KubedashConfigurationComponent, - KubernetesPodContainersComponent + KubernetesPodContainersComponent, ], providers: [ KubernetesService, @@ -170,7 +170,7 @@ import { KubernetesSummaryTabComponent } from './tabs/kubernetes-summary-tab/kub KubernetesPodStatusComponent, KubeServiceCardComponent, KubernetesResourceViewerComponent, - KubernetesPodContainersComponent + KubernetesPodContainersComponent, ], exports: [ KubernetesResourceViewerComponent diff --git a/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts b/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts index 45f33e0292..03989028b4 100644 --- a/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts +++ b/custom-src/frontend/app/custom/kubernetes/kubernetes.setup.module.ts @@ -14,6 +14,29 @@ import { KubernetesConfigAuthFormComponent, } from './auth-forms/kubernetes-config-auth-form/kubernetes-config-auth-form.component'; import { KubernetesGKEAuthFormComponent } from './auth-forms/kubernetes-gke-auth-form/kubernetes-gke-auth-form.component'; +import { KubeConfigImportComponent } from './kube-config-registration/kube-config-import/kube-config-import.component'; +import { + KubeConfigTableImportStatusComponent, +} from './kube-config-registration/kube-config-import/kube-config-table-import-status/kube-config-table-import-status.component'; +import { KubeConfigRegistrationComponent } from './kube-config-registration/kube-config-registration.component'; +import { + KubeConfigSelectionComponent, +} from './kube-config-registration/kube-config-selection/kube-config-selection.component'; +import { + KubeConfigTableCertComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-cert/kube-config-table-cert.component'; +import { + KubeConfigTableName, +} from './kube-config-registration/kube-config-selection/kube-config-table-name/kube-config-table-name.component'; +import { + KubeConfigTableSelectComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-select/kube-config-table-select.component'; +import { + KubeConfigTableSubTypeSelectComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-sub-type-select/kube-config-table-sub-type-select.component'; +import { + KubeConfigTableUserSelectComponent, +} from './kube-config-registration/kube-config-selection/kube-config-table-user-select/kube-config-table-user-select.component'; import { kubeEntityCatalog } from './kubernetes-entity-catalog'; import { KUBERNETES_ENDPOINT_TYPE } from './kubernetes-entity-factory'; import { generateKubernetesEntities } from './kubernetes-entity-generator'; @@ -21,7 +44,6 @@ import { BaseKubeGuid } from './kubernetes-page.types'; import { KubernetesStoreModule } from './kubernetes.store.module'; import { KubernetesEndpointService } from './services/kubernetes-endpoint.service'; - @NgModule({ imports: [ EntityCatalogModule.forFeature(generateKubernetesEntities), @@ -35,6 +57,15 @@ import { KubernetesEndpointService } from './services/kubernetes-endpoint.servic KubernetesAWSAuthFormComponent, KubernetesConfigAuthFormComponent, KubernetesGKEAuthFormComponent, + KubeConfigRegistrationComponent, + KubeConfigSelectionComponent, + KubeConfigImportComponent, + KubeConfigTableSelectComponent, + KubeConfigTableUserSelectComponent, + KubeConfigTableImportStatusComponent, + KubeConfigTableSubTypeSelectComponent, + KubeConfigTableName, + KubeConfigTableCertComponent ], providers: [ BaseKubeGuid, @@ -45,6 +76,13 @@ import { KubernetesEndpointService } from './services/kubernetes-endpoint.servic KubernetesAWSAuthFormComponent, KubernetesConfigAuthFormComponent, KubernetesGKEAuthFormComponent, + KubeConfigRegistrationComponent, + KubeConfigTableSelectComponent, + KubeConfigTableUserSelectComponent, + KubeConfigTableImportStatusComponent, + KubeConfigTableSubTypeSelectComponent, + KubeConfigTableName, + KubeConfigTableCertComponent ] }) export class KubernetesSetupModule { diff --git a/custom-src/frontend/assets/custom/kube_import.png b/custom-src/frontend/assets/custom/kube_import.png new file mode 100644 index 0000000000..62d9f0bdd2 Binary files /dev/null and b/custom-src/frontend/assets/custom/kube_import.png differ diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html index 9b9fcb9301..3f634c71e4 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.html @@ -2,4 +2,5 @@ -{{row.value}} \ No newline at end of file +{{row.value}} \ No newline at end of file diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss index eb9c8d43a8..6cd7d23bf0 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/app-variables/table-cell-edit-variable/table-cell-edit-variable.component.scss @@ -7,3 +7,7 @@ mat-form-field { .cell-edit-variable { width: 100%; } + +.cell-value-variable { + cursor: pointer; +} diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts index f2bf84af31..d5018ccb88 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-quotas/cf-quotas-data-source.service.ts @@ -50,7 +50,7 @@ export class CfQuotasDataSourceService extends ListDataSource { map(requestInfo => ({ deleting: requestInfo.deleting.busy, error: requestInfo.deleting.error, - message: requestInfo.deleting.error ? `Failed to delete quota: ${requestInfo.message}` : null + message: requestInfo.deleting.error ? `Failed to delete quota: ${requestInfo.deleting.message}` : null })) ); }; diff --git a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts index 4a880b804b..ccfb93879a 100644 --- a/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts +++ b/src/frontend/packages/cloud-foundry/src/shared/components/list/list-types/cf-space-quotas/cf-space-quotas-data-source.service.ts @@ -50,7 +50,7 @@ export class CfOrgSpaceQuotasDataSourceService extends ListDataSource ({ deleting: requestInfo.deleting.busy, error: requestInfo.deleting.error, - message: requestInfo.deleting.error ? `Failed to delete space quota: ${requestInfo.message}` : null + message: requestInfo.deleting.error ? `Failed to delete space quota: ${requestInfo.deleting.message}` : null })) ); }; diff --git a/src/frontend/packages/core/sass/_all-theme.scss b/src/frontend/packages/core/sass/_all-theme.scss index 4f657dbc93..9fc21b86f9 100644 --- a/src/frontend/packages/core/sass/_all-theme.scss +++ b/src/frontend/packages/core/sass/_all-theme.scss @@ -181,6 +181,6 @@ $side-nav-light-active: #484848; $warn: map-get($theme, warn); $primary: map-get($theme, primary); $white: #fff; // Use default palette for status - @return (success: map-get($mat-green, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, ); + @return (success: map-get($mat-green, 500), warning: map-get($mat-orange, 500), danger: mat-color($warn), tentative: map-get($mat-grey, 500), busy: mat-color($primary), text: $white, info: map-get($mat-blue, 500)); } } diff --git a/src/frontend/packages/core/sass/components/mat-table.scss b/src/frontend/packages/core/sass/components/mat-table.scss index c623288894..6c0d902e78 100644 --- a/src/frontend/packages/core/sass/components/mat-table.scss +++ b/src/frontend/packages/core/sass/components/mat-table.scss @@ -12,7 +12,7 @@ $mat-table-header-paginator-font-size: 13px; .stratos { // Only put right padding between cells - .mat-cell, .mat-header-cell { + .mat-cell, mat-footer-cell, mat-header-cell { padding: 10px 10px 10px 0; } diff --git a/src/frontend/packages/core/src/core/utils.service.ts b/src/frontend/packages/core/src/core/utils.service.ts index 415a8f89c6..83de981544 100644 --- a/src/frontend/packages/core/src/core/utils.service.ts +++ b/src/frontend/packages/core/src/core/utils.service.ts @@ -349,3 +349,11 @@ export const arraysEqual = (a: any[], b: any[]): boolean => { // Falsy/Truthy return false; }; + +export const createGuid = (): string => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + var r = Math.random() * 16 | 0, + v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} diff --git a/src/frontend/packages/core/src/features/endpoints/connect.service.ts b/src/frontend/packages/core/src/features/endpoints/connect.service.ts index 015ff5b14d..d4e9dac73d 100644 --- a/src/frontend/packages/core/src/features/endpoints/connect.service.ts +++ b/src/frontend/packages/core/src/features/endpoints/connect.service.ts @@ -201,6 +201,25 @@ export class ConnectEndpointService { ); } + public getConnectingObservable() { + return this.isBusy$.pipe( + pairwise(), + filter(([oldBusy, newBusy]) => { + return !(oldBusy === true && newBusy === false); + }), + withLatestFrom(this.update$), + map(([, updateSection]) => ({ + ...updateSection, + completed: !updateSection.busy, + })), + startWith({ + busy: true, + completed: false, + error: false + }) + ); + } + public destroy() { safeUnsubscribe(...this.subs); } diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html index 0aa360488a..5a3116dce9 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.html @@ -1,5 +1,5 @@ -

Register a new Endpoint

+

Register Endpoint

diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts index 85fedb80c1..e354112437 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint-base-step/create-endpoint-base-step.component.ts @@ -1,16 +1,16 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { RouterNav } from '../../../../../../store/src/actions/router.actions'; import { GeneralEntityAppState } from '../../../../../../store/src/app-state'; -import { selectSessionData } from '../../../../../../store/src/reducers/auth.reducer'; import { entityCatalog } from '../../../../../../store/src/entity-catalog/entity-catalog'; +import { IStratosEndpointDefinition } from '../../../../../../store/src/entity-catalog/entity-catalog.types'; +import { selectSessionData } from '../../../../../../store/src/reducers/auth.reducer'; import { BASE_REDIRECT_QUERY } from '../../../../shared/components/stepper/stepper.types'; import { TileConfigManager } from '../../../../shared/components/tile/tile-selector.helpers'; import { ITileConfig, ITileData } from '../../../../shared/components/tile/tile-selector.types'; -import { Observable } from 'rxjs'; -import { IStratosEndpointDefinition } from '../../../../../../store/src/entity-catalog/entity-catalog.types'; interface ICreateEndpointTilesData extends ITileData { type: string; @@ -103,7 +103,8 @@ export class CreateEndpointBaseStepComponent { }, { type: endpoint.type, - parentType: endpoint.parentType + parentType: endpoint.parentType, + component: endpoint.registrationComponent, } ); }); diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html index d4a8beb761..076abff82d 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.html @@ -1,8 +1,8 @@ -

Register a new Endpoint

+

Register Endpoint

- + @@ -11,4 +11,6 @@

Register a new Endpoint

[finishButtonText]="connect.doConnect ? 'Connect' : 'Finish'">
-
\ No newline at end of file + + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts index 1bd0f9c07c..d44c7af725 100644 --- a/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts +++ b/src/frontend/packages/core/src/features/endpoints/create-endpoint/create-endpoint.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild, ViewContainerRef, ComponentRef, OnInit, OnDestroy, ComponentFactory, ComponentFactoryResolver } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog'; @@ -10,16 +10,40 @@ import { getIdFromRoute } from '../../../core/utils.service'; templateUrl: './create-endpoint.component.html', styleUrls: ['./create-endpoint.component.scss'] }) -export class CreateEndpointComponent { +export class CreateEndpointComponent implements OnInit, OnDestroy { showConnectStep: boolean; - constructor(activatedRoute: ActivatedRoute) { + component: any; + @ViewChild('customComponent', { read: ViewContainerRef, static: true }) customComponentContainer; + componentRef: ComponentRef; + + constructor(activatedRoute: ActivatedRoute, private resolver: ComponentFactoryResolver) { const epType = getIdFromRoute(activatedRoute, 'type'); const epSubType = getIdFromRoute(activatedRoute, 'subtype'); const endpoint = entityCatalog.getEndpoint(epType, epSubType); + + this.component = endpoint.definition.registrationComponent; this.showConnectStep = !endpoint.definition.unConnectable ? endpoint.definition.authTypes && !!endpoint.definition.authTypes.length : false; } + + ngOnInit() { + this.customComponentContainer.clear(); + if (this.componentRef) { + this.componentRef.destroy(); + } + if (this.component) { + const factory: ComponentFactory = this.resolver.resolveComponentFactory(this.component); + this.componentRef = this.customComponentContainer.createComponent(factory); + } + } + + ngOnDestroy() { + if (this.componentRef) { + this.componentRef.destroy(); + } + } + } diff --git a/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts b/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts index 6637ca8748..fc0663320a 100644 --- a/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts +++ b/src/frontend/packages/core/src/shared/components/app-action-monitor-icon/app-action-monitor-icon.component.ts @@ -36,7 +36,8 @@ export class ActionMonitorComponentState { this.currentState = this.getStateObservable(entityMonitor, monitorState); } - private getStateObservable(entityMonitor: EntityMonitor, monitorState: AppMonitorComponentTypes) { + private getStateObservable(entityMonitor: EntityMonitor, monitorState: AppMonitorComponentTypes) + : Observable { switch (monitorState) { case AppMonitorComponentTypes.DELETE: return this.getDeletingState(entityMonitor); @@ -122,6 +123,10 @@ export class ActionMonitorComponentState { }) export class AppActionMonitorIconComponent implements OnInit { + // State observable - use this instead of creating one + @Input() + public state: Observable; + @Input() public entityKey: string; @@ -143,6 +148,9 @@ export class AppActionMonitorIconComponent implements OnInit { constructor(private entityMonitorFactory: EntityMonitorFactory) { } ngOnInit() { + if (this.state) { + this.currentState = this.state; + } else { const state: ActionMonitorComponentState = new ActionMonitorComponentState( this.entityMonitorFactory, this.id, @@ -151,5 +159,6 @@ export class AppActionMonitorIconComponent implements OnInit { this.updateKey ); this.currentState = state.currentState; + } } } diff --git a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html index 6e9a288393..1c9713853c 100644 --- a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html +++ b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.html @@ -1 +1,4 @@ - +
+ + +
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts index 2895bce18d..345af4a5c6 100644 --- a/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts +++ b/src/frontend/packages/core/src/shared/components/app-action-monitor/app-action-monitor.component.ts @@ -1,8 +1,7 @@ -import { DataSource } from '@angular/cdk/table'; import { Component, Input, OnInit } from '@angular/core'; import { schema } from 'normalizr'; import { never as observableNever, Observable, of as observableOf } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, publishReplay, refCount } from 'rxjs/operators'; import { EntitySchema } from '../../../../../store/src/helpers/entity-schema'; import { EntityMonitorFactory } from '../../../../../store/src/monitors/entity-monitor.factory.service'; @@ -26,7 +25,7 @@ import { ITableColumn } from '../list/list-table/table.types'; export class AppActionMonitorComponent implements OnInit { @Input() - private data$: Observable> = observableNever(); + public data$: Observable> = observableNever(); @Input() public entityKey: string; @@ -58,7 +57,7 @@ export class AppActionMonitorComponent implements OnInit { @Input() public columns: ITableColumn[] = []; - public dataSource: DataSource; + public dataSource: ITableListDataSource; public allColumns: ITableColumn[] = []; @@ -82,9 +81,16 @@ export class AppActionMonitorComponent implements OnInit { cellFlex: '0 0 24px' }; + // Some obs will only ever emit once, once consumed in template this meant table never received emitted data + // so wrap in publish replay + const replayData = this.data$.pipe( + publishReplay(1), + refCount() + ) + this.allColumns = [...this.columns, monitorColumn]; this.dataSource = { - connect: () => this.data$, + connect: () => replayData, disconnect: () => { }, trackBy: (index, item) => { const fn = monitorColumn.cellConfig(item).getId; @@ -117,7 +123,7 @@ export class AppActionMonitorComponent implements OnInit { }) ); } - } as ITableListDataSource; + }; } diff --git a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html index 195ae17f56..edef8e7b79 100644 --- a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html +++ b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.html @@ -1,6 +1,6 @@
- +
No file selected
{{ name }}
@@ -10,4 +10,5 @@
+
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts index a08b287010..b99327bd0a 100644 --- a/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts +++ b/src/frontend/packages/core/src/shared/components/file-input/file-input.component.ts @@ -28,8 +28,12 @@ export class FileInputComponent implements OnInit, OnDestroy { @Input() accept: string; @Output() onFileSelect: EventEmitter = new EventEmitter(); + @Output() onFileData: EventEmitter = new EventEmitter(); + @Input() fileFormControlName; + @Input() buttonLabel = ''; + private files: File[]; public name = ''; @@ -65,7 +69,9 @@ export class FileInputComponent implements OnInit, OnDestroy { this.onFileSelect.emit(this.files[0]); if (!!this.formGroupControl) { - this.handleFormControl(this.files[0]); + this.handleFileData(this.files[0], (value) => this.updateFileState(value)); + } else { + this.handleFileData(this.files[0], (value) => this.onFileData.emit(value)); } if (this.files.length > 0) { this.name = this.files[0].name; @@ -79,17 +85,18 @@ export class FileInputComponent implements OnInit, OnDestroy { return false; } - handleFormControl(file) { + handleFileData(file, done) { const reader = new FileReader(); reader.onload = () => { - this.updateFileState(reader.result); + done(reader.result); }; reader.onerror = () => { // Clear the form and thus make it invalid on error - this.updateFileState(null); + done(null); }; reader.readAsText(file); } + private updateFileState(value: string | ArrayBuffer) { this.formGroupControl.control.controls[this.fileFormControlName].setValue(value); } diff --git a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts index 4b1af3bd34..96c4b0443e 100644 --- a/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts +++ b/src/frontend/packages/core/src/shared/components/list/data-sources-controllers/list-data-source-types.ts @@ -64,11 +64,29 @@ interface ICoreListDataSource extends DataSource { trackBy(index: number, item: T); } -export interface ITableListDataSource extends ICoreListDataSource { +interface ICoreTableListDataSource extends ICoreListDataSource { + isTableLoading$?: Observable; + + selectAllChecked?: boolean; // Select items - remove once ng-content can exist in md-table + selectAllIndeterminate?: boolean; // Select all checkbox as indeterminate + selectedRows?: Map; // Select items - remove once ng-content can exist in md-table + selectedRows$?: ReplaySubject>; // Select items - remove once ng-content can exist in md-table + selectAllFilteredRows?: () => void; // Select items - remove once ng-content can exist in md-table + selectedRowToggle?: (row: T, multiMode?: boolean) => void; // Select items - remove once ng-content can exist in md-table + selectClear?: () => void; + + editRow?: T; // Edit items - remove once ng-content can exist in md-table + startEdit?: (row: T) => void; // Edit items - remove once ng-content can exist in md-table + saveEdit?: () => void; // Edit items - remove once ng-content can exist in md-table + cancelEdit?: () => void; // Edit items - remove once ng-content can exist in md-table + getRowUniqueId?: getRowUniqueId; +} + +export interface ITableListDataSource extends ICoreTableListDataSource { isTableLoading$: Observable; } -export interface IListDataSource extends ICoreListDataSource, EntityCatalogEntityConfig { +export interface IListDataSource extends ICoreListDataSource, ICoreTableListDataSource, EntityCatalogEntityConfig { pagination$: Observable; isLocal?: boolean; localDataFunctions?: (( @@ -94,20 +112,11 @@ export interface IListDataSource extends ICoreListDataSource, EntityCatalo filter$: Observable; sort$: Observable; - editRow: T; // Edit items - remove once ng-content can exist in md-table - selectAllChecked: boolean; // Select items - remove once ng-content can exist in md-table - selectedRows: Map; // Select items - remove once ng-content can exist in md-table - selectedRows$: ReplaySubject>; // Select items - remove once ng-content can exist in md-table + getRowUniqueId: getRowUniqueId; entitySelectConfig?: EntitySelectConfig; // For multi action lists, this is used to configure the entity select. - selectAllFilteredRows(); // Select items - remove once ng-content can exist in md-table - selectedRowToggle(row: T, multiMode?: boolean); // Select items - remove once ng-content can exist in md-table - selectClear(); - startEdit(row: T); // Edit items - remove once ng-content can exist in md-table - saveEdit(); // Edit items - remove once ng-content can exist in md-table - cancelEdit(); // Edit items - remove once ng-content can exist in md-table destroy(); /** * Set's data source specific text filter param diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html index 125c4a88fc..7762b74c15 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.html @@ -1,11 +1,14 @@ -
- - - -
+ + + + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss index 9681b2e526..e964f6220f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.scss @@ -1,4 +1,14 @@ -div { +.edit { display: flex; justify-content: flex-end; + &--subtle { + .mat-icon-button { + font-size: 18px; + height: 25px; + line-height: 25px; + opacity: 60%; + padding-left: 5px; + width: 35px; + } + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts index 5f38b44101..1aa4769a4f 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-cell-edit/table-cell-edit.component.ts @@ -1,5 +1,6 @@ -/* tslint:disable:no-access-missing-member https://github.com/mgechev/codelyzer/issues/191*/ -import { Component, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; + +import { IListDataSource } from '../../data-sources-controllers/list-data-source-types'; import { TableCellCustom } from '../../list.types'; @Component({ @@ -7,4 +8,20 @@ import { TableCellCustom } from '../../list.types'; templateUrl: './table-cell-edit.component.html', styleUrls: ['./table-cell-edit.component.scss'] }) -export class TableCellEditComponent extends TableCellCustom { } +export class TableCellEditComponent extends TableCellCustom { + + @Input() + row: T; + + @Input() + dataSource: IListDataSource; + + @Input() + subtle: boolean; + + isEditing(): boolean { + return this.dataSource.editRow ? + this.dataSource.getRowUniqueId(this.row) === this.dataSource.getRowUniqueId(this.dataSource.editRow) : + false + } +} diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html index c2f860861e..51c00db460 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-header-select/table-header-select.component.html @@ -1,2 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html index be2ec31af4..b715c964f2 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.html @@ -1,5 +1,5 @@
+ [ngClass]="{'table-row-wrapper__blocked': isBlocked$ | async, 'table-row-wrapper__info': inInfoState$ | async, 'table-row-wrapper__warning': inWarningState$ | async,'table-row-wrapper__errored': inErrorState$ | async}">
Deleting
@@ -7,7 +7,7 @@
+ [ngClass]="{'in-expanded-row': !!inExpandedRow, 'has-expanded-row': expandComponent, 'has-error-row': errorMessage$ | async}">
- warning -
+
+
+ warning + info +
\ No newline at end of file diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss index 06621f3b97..535edff498 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.scss @@ -16,7 +16,8 @@ } } &__errored, - &__warning { + &__warning, + &__info { .table-row__error { display: flex; } @@ -64,14 +65,23 @@ &__error { align-items: center; display: none; + &-message { flex: 1; line-height: 20px; - margin: 15px 36px; + margin-left: 10px; text-align: left; } - &-icon { - padding-left: 24px; + &-spacer { + align-self: stretch; + flex: 0 0 20px; + &__prominentErrorBar { + flex: 0 0 68px; + } + } + &-content { + flex: 1; + padding: 0 10px 10px 0; } } &__blocker { @@ -93,6 +103,7 @@ } .table-row__inner__expansion.mat-expansion-panel { + border-radius: unset; width: 100%; .table-row__inner__expansion--header { @@ -108,4 +119,9 @@ &.in-expanded-row { border-left-width: 1px; } + + &.has-error-row { + // Remove the bottom border if there's an error underneath it + border-bottom-width: 0; + } } diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss index 0ac4538dad..07be004cb3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.theme.scss @@ -2,6 +2,7 @@ $status-colors: map-get($app-theme, status); $error-color: map-get($status-colors, danger); $warn-color: map-get($status-colors, warning); + $info-color: map-get($status-colors, info); $text-color: map-get($status-colors, text); $primary: map-get($theme, primary); $primary-color: mat-color($primary); @@ -22,26 +23,24 @@ } .table-row-wrapper { &__errored { - .table-row { - background-color: transparentize($error-color, .9); - } - .table-row__error { - background-color: $error-color; - color: $text-color; - } - .table-row__error-message { - a { - color: $text-color; + .table-row__error-content { + mat-icon { + color: $error-color; } } } &__warning { - .table-row { - background-color: transparentize($warn-color, .9); + .table-row__error-content { + mat-icon { + color: $warn-color; + } } - .table-row__error { - background-color: $warn-color; - color: $text-color; + } + &__info { + .table-row__error-content { + mat-icon { + color: $info-color; + } } } &__highlighted { diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts index 1c3c08d120..2c8325d488 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table-row/table-row.component.ts @@ -38,13 +38,16 @@ export class TableRowComponent extends CdkRow implements OnInit { @Input() minRowHeight: string; @Input() inExpandedRow: boolean; @Input() rowId: string; + @Input() prominentErrorBar: boolean; public inErrorState$: Observable; public inWarningState$: Observable; + public inInfoState$: Observable; public errorMessage$: Observable; public isBlocked$: Observable; public isHighlighted$: Observable; public isDeleting$: Observable; + public isWarningIcon$: Observable; public defaultMinRowHeight = '50px'; private expandedComponentRef: ComponentRef; @@ -64,6 +67,9 @@ export class TableRowComponent extends CdkRow implements OnInit { this.inWarningState$ = this.rowState.pipe( map(state => state.warning) ); + this.inInfoState$ = this.rowState.pipe( + map(state => state.info) + ); this.errorMessage$ = this.rowState.pipe( map(state => state.message) ); @@ -76,6 +82,9 @@ export class TableRowComponent extends CdkRow implements OnInit { this.isDeleting$ = this.rowState.pipe( map(state => state.deleting) ); + this.isWarningIcon$ = this.rowState.pipe( + map(state => state.error || state.warning) + ); } // Ensure we 'register' with the expander service. This also helps with page changes diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html index 4bd2ce8004..fe4fa52887 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.html @@ -37,7 +37,7 @@ + [rowId]="dataSource.trackBy(null, row)" [prominentErrorBar]="prominentErrorBar"> diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss index 8c2db26e14..c2dc1fe36a 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.scss @@ -5,7 +5,6 @@ mat-cell, mat-header-cell { flex: 1 1 200px; - padding: 10px; app-table-cell { width: 100%; @@ -88,6 +87,19 @@ mat-header-cell { } } + &--table-centred { + app-table-cell { + align-items: center; + display: flex; + justify-content: center; + text-align: center; + } + } + + &--table-no-v-padding { + padding: 0 10px 0 0; + } + &--table-column-additional-padding { app-table-cell { padding-left: 15px; diff --git a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts index 10f0637f6a..ab1af731f3 100644 --- a/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts +++ b/src/frontend/packages/core/src/shared/components/list/list-table/table.component.ts @@ -64,6 +64,7 @@ export class TableComponent implements OnInit, OnDestroy { public columnNames: string[]; @Input() minRowHeight: string; + @Input() prominentErrorBar: boolean = true; ngOnInit() { if (this.addSelect || this.expandComponent || this.addActions) { diff --git a/src/frontend/packages/core/src/shared/shared.module.ts b/src/frontend/packages/core/src/shared/shared.module.ts index d5ab5af42e..b7cc5252d8 100644 --- a/src/frontend/packages/core/src/shared/shared.module.ts +++ b/src/frontend/packages/core/src/shared/shared.module.ts @@ -47,6 +47,7 @@ import { MetaCardItemComponent } from './components/list/list-cards/meta-card/me import { MetaCardKeyComponent } from './components/list/list-cards/meta-card/meta-card-key/meta-card-key.component'; import { MetaCardTitleComponent } from './components/list/list-cards/meta-card/meta-card-title/meta-card-title.component'; import { MetaCardValueComponent } from './components/list/list-cards/meta-card/meta-card-value/meta-card-value.component'; +import { TableCellEditComponent } from './components/list/list-table/table-cell-edit/table-cell-edit.component'; import { TableCellRequestMonitorIconComponent, } from './components/list/list-table/table-cell-request-monitor-icon/table-cell-request-monitor-icon.component'; @@ -310,7 +311,8 @@ import { UserPermissionDirective } from './user-permission.directive'; SidepanelPreviewComponent, TableCellEndpointNameComponent, CardProgressOverlayComponent, - MaxListMessageComponent + MaxListMessageComponent, + TableCellEditComponent ], entryComponents: [ DialogConfirmComponent, diff --git a/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts b/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts index 7205f8ff23..7f29f7a188 100644 --- a/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts +++ b/src/frontend/packages/store/src/entity-catalog/entity-catalog.types.ts @@ -67,6 +67,7 @@ export interface IStratosBaseEntityDefinition[]; readonly paginationConfig?: PaginationPageIteratorConfig; readonly tableConfig?: EntityTableConfig; + readonly registrationComponent?: any; /** * Hook that will fire before an entity is emitted by an entity service. This could be used, for example, entity validation */ diff --git a/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts b/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts index b62fee8a48..1a3826a515 100644 --- a/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts +++ b/src/frontend/packages/store/src/reducers/api-request-reducer/fail-request.ts @@ -26,8 +26,8 @@ export function failRequest(state, action: IFailedRequestAction) { busy: false, deleted: false, error: true, + message: action.message }; - requestFailedState.message = action.message; } else { requestFailedState.fetching = false; requestFailedState.error = true; diff --git a/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts b/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts index 8b514a0c4a..0f1b724a3e 100644 --- a/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts +++ b/src/frontend/packages/store/src/reducers/api-request-reducer/types.ts @@ -14,6 +14,14 @@ export interface ActionState { message: string; } +// Status of an action +export interface ActionStatus { + busy: boolean; + error: boolean; + message?: string; + completed: boolean; +} + /** * Multi action lists can have different entity types per page * We use schemaKey to track this type diff --git a/src/frontend/packages/store/src/selectors/api.selectors.ts b/src/frontend/packages/store/src/selectors/api.selectors.ts index 4aecb698d7..b1eaa34938 100644 --- a/src/frontend/packages/store/src/selectors/api.selectors.ts +++ b/src/frontend/packages/store/src/selectors/api.selectors.ts @@ -1,8 +1,8 @@ import { compose } from '@ngrx/store'; +import { GeneralEntityAppState, IRequestEntityTypeState as IRequestEntityKeyState, IRequestTypeState } from '../app-state'; import { EntityCatalogHelpers } from '../entity-catalog/entity-catalog.helper'; import { EntityCatalogEntityConfig } from '../entity-catalog/entity-catalog.types'; -import { GeneralEntityAppState, IRequestEntityTypeState as IRequestEntityKeyState, IRequestTypeState } from '../app-state'; import { ActionState, RequestInfoState, UpdatingSection } from '../reducers/api-request-reducer/types'; import { APIResource } from '../types/api.types'; @@ -31,7 +31,7 @@ export const getEntityUpdateSections = ( export const getUpdateSectionById = (guid: string) => ( updating ): ActionState => { - return updating[guid]; + return updating ? updating[guid] : null; }; export function selectUpdateInfo( diff --git a/src/jetstream/default.config.properties b/src/jetstream/default.config.properties index e45f461d9d..2a430adb51 100644 --- a/src/jetstream/default.config.properties +++ b/src/jetstream/default.config.properties @@ -47,8 +47,8 @@ INVITE_USER_CLIENT_SECRET= # Use local admin user rather than UAA users # AUTH_ENDPOINT_TYPE=local -# LOCAL_USER=localuser -# LOCAL_USER_PASSWORD=localuserpass +# LOCAL_USER=admin +# LOCAL_USER_PASSWORD=admin # LOCAL_USER_SCOPE=stratos.admin # MariaDB database for local dev diff --git a/src/jetstream/plugins/kubernetes/main.go b/src/jetstream/plugins/kubernetes/main.go index 0ed9278022..4556391650 100644 --- a/src/jetstream/plugins/kubernetes/main.go +++ b/src/jetstream/plugins/kubernetes/main.go @@ -4,8 +4,10 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/http" "net/url" "strconv" + "strings" "errors" @@ -135,7 +137,7 @@ func (c *KubernetesSpecification) Init() error { } func (c *KubernetesSpecification) AddAdminGroupRoutes(echoGroup *echo.Group) { - // no-op + echoGroup.GET("/kube/cert", c.RequiresCert) } func (c *KubernetesSpecification) AddSessionGroupRoutes(echoGroup *echo.Group) { @@ -242,3 +244,31 @@ func parseErrorResponse(body []byte) error { func (c *KubernetesSpecification) UpdateMetadata(info *interfaces.Info, userGUID string, echoContext echo.Context) { } + +func (c *KubernetesSpecification) RequiresCert(ec echo.Context) error { + url := ec.QueryParam("url") + + log.Debug("Request Kube API Versions") + var httpClient = c.portalProxy.GetHttpClient(false) + _, err := httpClient.Get(url + "/api") + var response struct { + Status int + Required bool + Error bool + Message string + } + if err != nil { + if strings.Contains(err.Error(), "x509: certificate") { + response.Status = http.StatusOK + response.Required = true + } else { + response.Status = http.StatusInternalServerError + response.Error = true + response.Message = fmt.Sprintf("Failed to validate Kube certificate requirement: %+v", err) + } + } else { + response.Status = http.StatusOK + response.Required = false + } + return ec.JSON(response.Status, response) +} diff --git a/src/test-e2e/endpoints/register-dialog.po.ts b/src/test-e2e/endpoints/register-dialog.po.ts index 30e52790e7..44423a0d97 100644 --- a/src/test-e2e/endpoints/register-dialog.po.ts +++ b/src/test-e2e/endpoints/register-dialog.po.ts @@ -18,7 +18,7 @@ export class RegisterStepper extends Page { } isRegisterDialog(): promise.Promise { - return this.header.getTitleText().then(title => title === 'Register a new Endpoint'); + return this.header.getTitleText().then(title => title === 'Register Endpoint'); } getName = () => this.form.getFormField('name');