From 6a02712226dca46b0844af9d03b0986abef0fb2d Mon Sep 17 00:00:00 2001 From: Orfeas Kourkakis Date: Mon, 30 Jan 2023 12:02:05 +0200 Subject: [PATCH] vwa(front): Prevent PVCs from being deleted when there is a corresponding notebook (kubeflow/kubeflow#6899) * vwa(back): Append notebooks using each PVC Append notebooks using each PVC when getting and parsing all PVCs. Refs arrikto/dev#2017 Signed-off-by: Orfeas Kourkakis Reviewed-by: Kimonas Sotirchos * vwa(front): Create custom delete action column Create custom delete column component that extends ActionComponent from Kubeflow common code and adds the following functionalities: * Disable the button when a row's notebooks array is not empty * Display an appropriate message including the notebooks' names Refs arrikto/dev#2017 Signed-off-by: Orfeas Kourkakis Reviewed-by: Kimonas Sotirchos * vwa(front): Add delete button component in index page Implement custom delete column in the table in VWA's index page. Refs arrikto/dev#2017 Signed-off-by: Orfeas Kourkakis Reviewed-by: Kimonas Sotirchos * vwa(front): Add Used by column in volumes table Add a Used by column in volumes table of VWA's index page in order to link to the notebooks that are using each PVC. Refs arrikto/dev#2017 Signed-off-by: Orfeas Kourkakis Reviewed-by: Kimonas Sotirchos * vwa(front): Fix integration tests Update mock request for PVCs Refs arrikto/dev#2017 Refs arrikto/dev#2135 Signed-off-by: Orfeas Kourkakis Reviewed-by: Kimonas Sotirchos * vwa(front): Fix format error Signed-off-by: Orfeas Kourkakis * web-apps(front): Fix linting errors Signed-off-by: Orfeas Kourkakis --------- Signed-off-by: Orfeas Kourkakis --- .../component-value.component.ts | 19 +++++++- .../resource-table/resource-table.module.ts | 2 +- .../resource-table/table/table.component.html | 1 + .../resource-table/table/table.component.ts | 4 +- .../projects/kubeflow/src/public-api.ts | 4 ++ .../volumes/backend/apps/common/utils.py | 36 ++++++++++++++- .../backend/apps/default/routes/get.py | 8 +++- .../volumes/backend/apps/rok/routes/get.py | 6 ++- .../volumes/backend/apps/rok/utils.py | 4 +- .../frontend/cypress/fixtures/pvcs.json | 6 +++ .../volumes/frontend/src/app/app.module.ts | 2 + .../app/pages/index/columns/columns.module.ts | 19 ++++++++ .../delete-button.component.html | 33 ++++++++++++++ .../delete-button.component.scss | 0 .../delete-button.component.spec.ts | 41 +++++++++++++++++ .../delete-button/delete-button.component.ts | 45 +++++++++++++++++++ .../columns/used-by/used-by.component.html | 23 ++++++++++ .../columns/used-by/used-by.component.scss | 9 ++++ .../columns/used-by/used-by.component.spec.ts | 41 +++++++++++++++++ .../columns/used-by/used-by.component.ts | 33 ++++++++++++++ .../frontend/src/app/pages/index/config.ts | 13 ++++++ .../app/pages/index/index-default/config.ts | 27 ++++------- .../index-default/index-default.component.ts | 4 ++ .../src/app/pages/index/index-rok/config.ts | 25 ++++++----- .../volumes/frontend/src/app/types/backend.ts | 1 + 25 files changed, 368 insertions(+), 38 deletions(-) create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/columns.module.ts create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.html create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.scss create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.spec.ts create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.ts create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.html create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.scss create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.spec.ts create mode 100644 components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.ts diff --git a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/component-value/component-value.component.ts b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/component-value/component-value.component.ts index c24ba4e2..bb31ac76 100644 --- a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/component-value/component-value.component.ts +++ b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/component-value/component-value.component.ts @@ -1,5 +1,12 @@ -import { Component, Input, ComponentRef, OnInit } from '@angular/core'; -import { ComponentValue } from '../types'; +import { + Component, + Input, + ComponentRef, + OnInit, + Output, + EventEmitter, +} from '@angular/core'; +import { ActionEvent, ComponentValue } from '../types'; import { ComponentPortal, Portal } from '@angular/cdk/portal'; export interface TableColumnComponent { @@ -32,6 +39,9 @@ export class ComponentValueComponent implements OnInit { @Input() valueDescriptor: ComponentValue; + @Output() + emitter = new EventEmitter(); + ngOnInit() { this.portal = new ComponentPortal(this.valueDescriptor.component); } @@ -39,5 +49,10 @@ export class ComponentValueComponent implements OnInit { onAttach(ref: ComponentRef) { this.componentRef = ref; this.componentRef.instance.element = this.element; + /* eslint-disable */ + if (this.componentRef.instance?.['emitter']) { + this.componentRef.instance['emitter'] = this.emitter; + } + /* eslint-enable */ } } diff --git a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/resource-table.module.ts b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/resource-table.module.ts index 7749bc96..36a510c3 100644 --- a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/resource-table.module.ts +++ b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/resource-table.module.ts @@ -77,6 +77,6 @@ import { RouterModule } from '@angular/router'; TableComponent, ComponentValueComponent, ], - exports: [ResourceTableComponent, TableComponent], + exports: [ResourceTableComponent, TableComponent, ActionComponent], }) export class ResourceTableModule {} diff --git a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html index b10a8334..473ac1a3 100644 --- a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html +++ b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.html @@ -196,6 +196,7 @@ diff --git a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts index c49a2b4d..848042d2 100644 --- a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts +++ b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/lib/resource-table/table/table.component.ts @@ -594,7 +594,9 @@ export class TableComponent public actionTriggered(e: ActionEvent) { // Forward the emitted ActionEvent - this.actionsEmitter.emit(e); + if (e instanceof ActionEvent) { + this.actionsEmitter.emit(e); + } } public newButtonTriggered() { diff --git a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts index 7b194ea8..674f7689 100644 --- a/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts +++ b/components/crud-web-apps/common/frontend/kubeflow-common-lib/projects/kubeflow/src/public-api.ts @@ -108,3 +108,7 @@ export * from './lib/status-icon/status-icon.module'; export * from './lib/urls/urls.component'; export * from './lib/urls/urls.module'; export * from './lib/urls/types'; + +export * from './lib/icon/icon.component'; +export * from './lib/icon/icon.module'; +export * from './lib/resource-table/action/action.component'; diff --git a/components/crud-web-apps/volumes/backend/apps/common/utils.py b/components/crud-web-apps/volumes/backend/apps/common/utils.py index 6b04046d..69c96d20 100644 --- a/components/crud-web-apps/volumes/backend/apps/common/utils.py +++ b/components/crud-web-apps/volumes/backend/apps/common/utils.py @@ -3,7 +3,7 @@ from kubeflow.kubeflow.crud_backend import api, helpers from . import status -def parse_pvc(pvc): +def parse_pvc(pvc, notebooks): """ pvc: client.V1PersistentVolumeClaim @@ -14,6 +14,7 @@ def parse_pvc(pvc): except Exception: capacity = pvc.spec.resources.requests["storage"] + notebooks = get_notebooks_using_pvc(pvc.metadata.name, notebooks) parsed_pvc = { "name": pvc.metadata.name, "namespace": pvc.metadata.namespace, @@ -22,11 +23,44 @@ def parse_pvc(pvc): "capacity": capacity, "modes": pvc.spec.access_modes, "class": pvc.spec.storage_class_name, + "notebooks": notebooks, } return parsed_pvc +def get_notebooks_using_pvc(pvc, notebooks): + """Return a list of Notebooks that are using the given PVC.""" + mounted_notebooks = [] + + for nb in notebooks: + pvcs = get_notebook_pvcs(nb) + if pvc in pvcs: + mounted_notebooks.append(nb["metadata"]["name"]) + + return mounted_notebooks + + +def get_notebook_pvcs(nb): + """ + Return a list of PVC names that the given notebook is using. + + If it doesn't use any, then an empty list will be returned. + """ + pvcs = [] + if not nb["spec"]["template"]["spec"]["volumes"]: + return [] + + vols = nb["spec"]["template"]["spec"]["volumes"] + for vol in vols: + # Check if the volume is a pvc + if not vol.get("persistentVolumeClaim"): + continue + pvcs.append(vol["persistentVolumeClaim"]["claimName"]) + + return pvcs + + def get_pods_using_pvc(pvc, namespace): """ Return a list of Pods that are using the given PVC diff --git a/components/crud-web-apps/volumes/backend/apps/default/routes/get.py b/components/crud-web-apps/volumes/backend/apps/default/routes/get.py index d141c821..5f3bb3e1 100644 --- a/components/crud-web-apps/volumes/backend/apps/default/routes/get.py +++ b/components/crud-web-apps/volumes/backend/apps/default/routes/get.py @@ -10,23 +10,27 @@ log = logging.getLogger(__name__) def get_pvcs(namespace): # Return the list of PVCs pvcs = api.list_pvcs(namespace) - content = [utils.parse_pvc(pvc) for pvc in pvcs.items] + notebooks = api.list_notebooks(namespace)["items"] + content = [utils.parse_pvc(pvc, notebooks) for pvc in pvcs.items] return api.success_response("pvcs", content) + @bp.route("/api/namespaces//pvcs/") def get_pvc(namespace, pvc_name): pvc = api.get_pvc(pvc_name, namespace) return api.success_response("pvc", api.serialize(pvc)) + @bp.route("/api/namespaces//pvcs//pods") def get_pvc_pods(namespace, pvc_name): pods = utils.get_pods_using_pvc(pvc_name, namespace) return api.success_response("pods", api.serialize(pods)) + @bp.route("/api/namespaces//pvcs//events") def get_pvc_events(namespace, pvc_name): events = api.list_pvc_events(namespace, pvc_name).items - return api.success_response("events", api.serialize(events)) \ No newline at end of file + return api.success_response("events", api.serialize(events)) diff --git a/components/crud-web-apps/volumes/backend/apps/rok/routes/get.py b/components/crud-web-apps/volumes/backend/apps/rok/routes/get.py index 5568278e..8fd3f684 100644 --- a/components/crud-web-apps/volumes/backend/apps/rok/routes/get.py +++ b/components/crud-web-apps/volumes/backend/apps/rok/routes/get.py @@ -21,21 +21,25 @@ def get_pvcs(namespace): # Return the list of PVCs and the corresponding Viewer's state pvcs = api.list_pvcs(namespace) - content = [utils.parse_pvc(pvc, viewers) for pvc in pvcs.items] + notebooks = api.list_notebooks(namespace)["items"] + content = [utils.parse_pvc(pvc, viewers, notebooks) for pvc in pvcs.items] return api.success_response("pvcs", content) + @bp.route("/api/namespaces//pvcs/") def get_pvc(namespace, pvc_name): pvc = api.get_pvc(pvc_name, namespace) return api.success_response("pvc", api.serialize(pvc)) + @bp.route("/api/namespaces//pvcs//pods") def get_pvc_pods(namespace, pvc_name): pods = get_pods_using_pvc(pvc_name, namespace) return api.success_response("pods", api.serialize(pods)) + @bp.route("/api/namespaces//pvcs//events") def get_pvc_events(namespace, pvc_name): events = api.list_pvc_events(namespace, pvc_name).items diff --git a/components/crud-web-apps/volumes/backend/apps/rok/utils.py b/components/crud-web-apps/volumes/backend/apps/rok/utils.py index e8da9502..b7612925 100644 --- a/components/crud-web-apps/volumes/backend/apps/rok/utils.py +++ b/components/crud-web-apps/volumes/backend/apps/rok/utils.py @@ -39,7 +39,7 @@ def add_pvc_rok_annotations(pvc, body): pvc.metadata.labels = labels -def parse_pvc(pvc, viewers): +def parse_pvc(pvc, viewers, notebooks): """ pvc: client.V1PersistentVolumeClaim viewers: dict(Key:PVC Name, Value: Viewer's Status) @@ -47,7 +47,7 @@ def parse_pvc(pvc, viewers): Process the PVC and format it as the UI expects it. If a Viewer is active for that PVC, then include this information """ - parsed_pvc = common_utils.parse_pvc(pvc) + parsed_pvc = common_utils.parse_pvc(pvc, notebooks) parsed_pvc["viewer"] = viewers.get(pvc.metadata.name, status.STATUS_PHASE.UNINITIALIZED) diff --git a/components/crud-web-apps/volumes/frontend/cypress/fixtures/pvcs.json b/components/crud-web-apps/volumes/frontend/cypress/fixtures/pvcs.json index 5a5c4168..d0a522cc 100644 --- a/components/crud-web-apps/volumes/frontend/cypress/fixtures/pvcs.json +++ b/components/crud-web-apps/volumes/frontend/cypress/fixtures/pvcs.json @@ -7,6 +7,7 @@ "modes": ["ReadWriteOnce"], "name": "a-pvc-phase-ready-viewer-ready", "namespace": "kubeflow-user", + "notebooks": [], "status": { "message": "Bound", "phase": "ready", @@ -21,6 +22,7 @@ "modes": ["ReadWriteOnce"], "name": "a-pvc-phase-ready-viewer-uninitialized", "namespace": "kubeflow-user", + "notebooks": [], "status": { "message": "Bound", "phase": "ready", @@ -35,6 +37,7 @@ "modes": ["ReadWriteOnce"], "name": "a-pvc-phase-ready-viewer-waiting", "namespace": "kubeflow-user", + "notebooks": [], "status": { "message": "Bound", "phase": "ready", @@ -49,6 +52,7 @@ "modes": ["ReadWriteOnce"], "name": "a-pvc-phase-unvailable-viewer-unavailable", "namespace": "kubeflow-user", + "notebooks": [], "status": { "message": "Pending: This volume will be bound when its first consumer is created. E.g., when you first browse its contents, or attach it to a notebook server", "phase": "unavailable", @@ -63,6 +67,7 @@ "modes": ["ReadWriteOnce"], "name": "a-pvc-phase-waiting-viewer-uninitialized", "namespace": "kubeflow-user", + "notebooks": ["test-notebook-1", "test-notebook-2"], "status": { "message": "Bound", "phase": "waiting", @@ -77,6 +82,7 @@ "modes": ["ReadWriteOnce"], "name": "a-pvc-phase-warning-viewer-ready", "namespace": "kubeflow-user", + "notebooks": [], "status": { "message": "Bound", "phase": "warning", diff --git a/components/crud-web-apps/volumes/frontend/src/app/app.module.ts b/components/crud-web-apps/volumes/frontend/src/app/app.module.ts index 2400ba96..d9c34633 100644 --- a/components/crud-web-apps/volumes/frontend/src/app/app.module.ts +++ b/components/crud-web-apps/volumes/frontend/src/app/app.module.ts @@ -24,6 +24,7 @@ import { IndexRokComponent } from './pages/index/index-rok/index-rok.component'; import { HttpClientModule, HttpClient } from '@angular/common/http'; import { VolumeDetailsPageModule } from './pages/volume-details-page/volume-details-page.module'; +import { ColumnsModule } from './pages/index/columns/columns.module'; @NgModule({ declarations: [ @@ -45,6 +46,7 @@ import { VolumeDetailsPageModule } from './pages/volume-details-page/volume-deta KubeflowModule, HttpClientModule, VolumeDetailsPageModule, + ColumnsModule, ], providers: [ { provide: ErrorStateMatcher, useClass: ImmediateErrorStateMatcher }, diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/columns.module.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/columns.module.ts new file mode 100644 index 00000000..22216a74 --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/columns.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { DeleteButtonComponent } from './delete-button/delete-button.component'; +import { IconModule, KubeflowModule, UrlsModule } from 'kubeflow'; +import { UsedByComponent } from './used-by/used-by.component'; + +@NgModule({ + declarations: [DeleteButtonComponent, UsedByComponent], + imports: [ + CommonModule, + MatTooltipModule, + IconModule, + KubeflowModule, + UrlsModule, + ], + exports: [DeleteButtonComponent, UsedByComponent], +}) +export class ColumnsModule {} diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.html b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.html new file mode 100644 index 00000000..79c3e621 --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.html @@ -0,0 +1,33 @@ +
+ + + +
+ +
+ +
+ +
+
diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.scss b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.spec.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.spec.ts new file mode 100644 index 00000000..719b42fd --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeleteButtonComponent } from './delete-button.component'; + +const mockElement = { + age: 'Mon, 19 Sep 2022 16:39:10 GMT', + capacity: '5Gi', + class: 'rok', + modes: ['ReadWriteOnce'], + name: 'a0-new-image-workspace-d8pc2', + namespace: 'kubeflow-user', + notebooks: ['a0-new-image'], + status: { + message: 'Bound', + phase: 'ready', + state: '', + }, + viewer: 'uninitialized', +}; + +describe('DeleteButtonComponent', () => { + let component: DeleteButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DeleteButtonComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DeleteButtonComponent); + component = fixture.componentInstance; + component.element = mockElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.ts new file mode 100644 index 00000000..58e3a191 --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/delete-button/delete-button.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit } from '@angular/core'; +import { ActionComponent, ActionIconValue, STATUS_TYPE } from 'kubeflow'; +import { TableColumnComponent } from 'kubeflow/lib/resource-table/component-value/component-value.component'; +@Component({ + selector: 'app-delete-button', + templateUrl: './delete-button.component.html', + styleUrls: ['./delete-button.component.scss'], +}) +export class DeleteButtonComponent + extends ActionComponent + implements TableColumnComponent, OnInit { + set element(data: any) { + this.data = data; + } + get element() { + return this.data; + } + + ngOnInit(): void { + this.action = new ActionIconValue({ + name: 'delete', + tooltip: $localize`Delete Volume`, + color: 'warn', + field: 'deleteAction', + iconReady: 'material:delete', + }); + } + + isPhaseUnavailable(): boolean { + return this.status === STATUS_TYPE.UNAVAILABLE; + } + + isPhaseTerminating(): boolean { + return this.status === STATUS_TYPE.TERMINATING; + } + + getDisabledTooltip(element: any) { + let tooltip = `Cannot delete PVC because it is being used by the following notebooks:\n`; + + for (const nb of element.notebooks) { + tooltip += `\n ${nb}`; + } + return tooltip; + } +} diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.html b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.html new file mode 100644 index 00000000..74066c40 --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.html @@ -0,0 +1,23 @@ +
+ + + + +
+ + +
+

Notebooks

+
  • + +
  • +
    +
    diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.scss b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.scss new file mode 100644 index 00000000..3568d3f1 --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.scss @@ -0,0 +1,9 @@ +.used-by-container { + margin-top: 10px; + margin-bottom: 10px; +} + +.popoverCard { + max-width: 400px; + padding: 4px 12px 4px 4px; +} diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.spec.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.spec.ts new file mode 100644 index 00000000..a1bf436d --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UsedByComponent } from './used-by.component'; + +const mockElement = { + age: 'Mon, 19 Sep 2022 16:39:10 GMT', + capacity: '5Gi', + class: 'rok', + modes: ['ReadWriteOnce'], + name: 'a0-new-image-workspace-d8pc2', + namespace: 'kubeflow-user', + notebooks: ['a0-new-image'], + status: { + message: 'Bound', + phase: 'ready', + state: '', + }, + viewer: 'uninitialized', +}; + +describe('UsedByComponent', () => { + let component: UsedByComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UsedByComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UsedByComponent); + component = fixture.componentInstance; + component.element = mockElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.ts new file mode 100644 index 00000000..89b9d7ab --- /dev/null +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/columns/used-by/used-by.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from '@angular/core'; +import { TableColumnComponent } from 'kubeflow/lib/resource-table/component-value/component-value.component'; + +@Component({ + selector: 'app-used-by', + templateUrl: './used-by.component.html', + styleUrls: ['./used-by.component.scss'], +}) +export class UsedByComponent implements TableColumnComponent, OnInit { + public data: any; + + set element(data: any) { + this.data = data; + } + get element() { + return this.data; + } + + constructor() {} + + ngOnInit(): void {} + + get pvcName() { + return this.element.name; + } + + getUrlItem(nb: string, element: any) { + return { + name: nb, + url: `/jupyter/notebook/details/${element.namespace}/${nb}`, + }; + } +} diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/config.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/config.ts index f89eefa5..6a4f64de 100644 --- a/components/crud-web-apps/volumes/frontend/src/app/pages/index/config.ts +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/config.ts @@ -5,8 +5,10 @@ import { DateTimeValue, LinkValue, LinkType, + ComponentValue, } from 'kubeflow'; import { quantityToScalar } from '@kubernetes/client-node/dist/util'; +import { UsedByComponent } from './columns/used-by/used-by.component'; export const tableConfig: TableConfig = { columns: [ @@ -62,6 +64,17 @@ export const tableConfig: TableConfig = { value: new PropertyValue({ field: 'class', truncate: true }), sort: true, }, + { + matHeaderCellDef: $localize`Used by`, + matColumnDef: 'usedBy', + style: { 'max-width': '60px' }, + value: new ComponentValue({ + component: UsedByComponent, + }), + sort: true, + sortingPreprocessorFn: element => element.notebooks, + filteringPreprocessorFn: element => element.notebooks, + }, // the apps should import the actions they want ], diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/config.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/config.ts index fca37e67..967c34c2 100644 --- a/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/config.ts +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/config.ts @@ -1,28 +1,19 @@ -import { - ActionListValue, - ActionIconValue, - TableColumn, - TableConfig, -} from 'kubeflow'; +import { TableColumn, TableConfig, ComponentValue } from 'kubeflow'; import { tableConfig } from '../config'; +import { DeleteButtonComponent } from '../columns/delete-button/delete-button.component'; -const actionsCol: TableColumn = { +const customDeleteCol: TableColumn = { matHeaderCellDef: '', - matColumnDef: 'actions', - value: new ActionListValue([ - new ActionIconValue({ - name: 'delete', - tooltip: $localize`Delete Volume`, - color: 'warn', - field: 'deleteAction', - iconReady: 'material:delete', - }), - ]), + matColumnDef: 'customDelete', + style: { width: '40px' }, + value: new ComponentValue({ + component: DeleteButtonComponent, + }), }; export const defaultConfig: TableConfig = { title: tableConfig.title, dynamicNamespaceColumn: true, newButtonText: tableConfig.newButtonText, - columns: tableConfig.columns.concat(actionsCol), + columns: tableConfig.columns.concat(customDeleteCol), }; diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/index-default.component.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/index-default.component.ts index 57094d71..6f109862 100644 --- a/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/index-default.component.ts +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-default/index-default.component.ts @@ -154,6 +154,10 @@ export class IndexDefaultComponent implements OnInit, OnDestroy { } public parseDeletionActionStatus(pvc: PVCProcessedObject) { + if (pvc.notebooks.length) { + return STATUS_TYPE.UNAVAILABLE; + } + if (pvc.status.phase !== STATUS_TYPE.TERMINATING) { return STATUS_TYPE.READY; } diff --git a/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-rok/config.ts b/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-rok/config.ts index 5443abf6..80556aa9 100644 --- a/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-rok/config.ts +++ b/components/crud-web-apps/volumes/frontend/src/app/pages/index/index-rok/config.ts @@ -3,12 +3,15 @@ import { ActionIconValue, TableColumn, TableConfig, + ComponentValue, } from 'kubeflow'; import { tableConfig } from '../config'; +import { DeleteButtonComponent } from '../columns/delete-button/delete-button.component'; -const actionsCol: TableColumn = { +const browseCol: TableColumn = { matHeaderCellDef: '', - matColumnDef: 'actions', + matColumnDef: 'browse', + style: { 'padding-right': '0' }, value: new ActionListValue([ new ActionIconValue({ name: 'edit', @@ -18,19 +21,21 @@ const actionsCol: TableColumn = { iconInit: 'material:folder', iconReady: 'custom:folderSearch', }), - new ActionIconValue({ - name: 'delete', - tooltip: 'Delete Volume', - color: 'warn', - field: 'deleteAction', - iconReady: 'material:delete', - }), ]), }; +const customDeleteCol: TableColumn = { + matHeaderCellDef: ' ', + matColumnDef: 'customDelete', + style: { width: '40px', 'padding-left': '0' }, + value: new ComponentValue({ + component: DeleteButtonComponent, + }), +}; + export const rokConfig: TableConfig = { title: tableConfig.title, dynamicNamespaceColumn: true, newButtonText: tableConfig.newButtonText, - columns: tableConfig.columns.concat([actionsCol]), + columns: tableConfig.columns.concat([browseCol, customDeleteCol]), }; diff --git a/components/crud-web-apps/volumes/frontend/src/app/types/backend.ts b/components/crud-web-apps/volumes/frontend/src/app/types/backend.ts index 1651b42e..bc8adc65 100644 --- a/components/crud-web-apps/volumes/frontend/src/app/types/backend.ts +++ b/components/crud-web-apps/volumes/frontend/src/app/types/backend.ts @@ -21,6 +21,7 @@ export interface PVCResponseObject { name: string; namespace: string; status: Status; + notebooks: string[]; } export interface PVCProcessedObject extends PVCResponseObject {