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 <orfeas@arrikto.com> Reviewed-by: Kimonas Sotirchos <kimwnasptd@arrikto.com> * 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 <orfeas@arrikto.com> Reviewed-by: Kimonas Sotirchos <kimwnasptd@arrikto.com> * 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 <orfeas@arrikto.com> Reviewed-by: Kimonas Sotirchos <kimwnasptd@arrikto.com> * 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 <orfeas@arrikto.com> Reviewed-by: Kimonas Sotirchos <kimwnasptd@arrikto.com> * vwa(front): Fix integration tests Update mock request for PVCs Refs arrikto/dev#2017 Refs arrikto/dev#2135 Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com> Reviewed-by: Kimonas Sotirchos <kimwnasptd@arrikto.com> * vwa(front): Fix format error Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com> * web-apps(front): Fix linting errors Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com> --------- Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>
This commit is contained in:
parent
d2c76a0739
commit
6a02712226
|
|
@ -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<ActionEvent>();
|
||||
|
||||
ngOnInit() {
|
||||
this.portal = new ComponentPortal(this.valueDescriptor.component);
|
||||
}
|
||||
|
|
@ -39,5 +49,10 @@ export class ComponentValueComponent implements OnInit {
|
|||
onAttach(ref: ComponentRef<TableColumnComponent>) {
|
||||
this.componentRef = ref;
|
||||
this.componentRef.instance.element = this.element;
|
||||
/* eslint-disable */
|
||||
if (this.componentRef.instance?.['emitter']) {
|
||||
this.componentRef.instance['emitter'] = this.emitter;
|
||||
}
|
||||
/* eslint-enable */
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,6 @@ import { RouterModule } from '@angular/router';
|
|||
TableComponent,
|
||||
ComponentValueComponent,
|
||||
],
|
||||
exports: [ResourceTableComponent, TableComponent],
|
||||
exports: [ResourceTableComponent, TableComponent, ActionComponent],
|
||||
})
|
||||
export class ResourceTableModule {}
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@
|
|||
<lib-component-value
|
||||
[element]="element"
|
||||
[valueDescriptor]="col.value"
|
||||
(emitter)="actionTriggered($event)"
|
||||
></lib-component-value>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -594,8 +594,10 @@ export class TableComponent
|
|||
|
||||
public actionTriggered(e: ActionEvent) {
|
||||
// Forward the emitted ActionEvent
|
||||
if (e instanceof ActionEvent) {
|
||||
this.actionsEmitter.emit(e);
|
||||
}
|
||||
}
|
||||
|
||||
public newButtonTriggered() {
|
||||
const ev = new ActionEvent('newResourceButton', {});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,21 +10,25 @@ 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/<namespace>/pvcs/<pvc_name>")
|
||||
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/<namespace>/pvcs/<pvc_name>/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/<namespace>/pvcs/<pvc_name>/events")
|
||||
def get_pvc_events(namespace, pvc_name):
|
||||
events = api.list_pvc_events(namespace, pvc_name).items
|
||||
|
|
|
|||
|
|
@ -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/<namespace>/pvcs/<pvc_name>")
|
||||
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/<namespace>/pvcs/<pvc_name>/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/<namespace>/pvcs/<pvc_name>/events")
|
||||
def get_pvc_events(namespace, pvc_name):
|
||||
events = api.list_pvc_events(namespace, pvc_name).items
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<div>
|
||||
<!--Ready Phase-->
|
||||
<button
|
||||
*ngIf="isPhaseReady()"
|
||||
mat-icon-button
|
||||
matTooltip="{{ action.tooltip }}"
|
||||
matTooltipClass="custom-tooltip"
|
||||
[color]="action.color"
|
||||
(click)="emitClickedEvent()"
|
||||
>
|
||||
<lib-icon [icon]="action.iconReady"></lib-icon>
|
||||
</button>
|
||||
|
||||
<div
|
||||
matTooltipClass="custom-tooltip"
|
||||
[matTooltip]="'PVC is terminating at the moment.'"
|
||||
matTooltipClass="custom-tooltip"
|
||||
>
|
||||
<button *ngIf="isPhaseTerminating()" mat-icon-button disabled>
|
||||
<lib-icon [icon]="action.iconInit"></lib-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
matTooltipClass="custom-tooltip"
|
||||
[matTooltip]="getDisabledTooltip(element)"
|
||||
matTooltipClass="custom-tooltip"
|
||||
>
|
||||
<button *ngIf="isPhaseUnavailable()" mat-icon-button disabled>
|
||||
<lib-icon [icon]="action.iconInit"></lib-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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<DeleteButtonComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<div class="used-by-container truncate">
|
||||
<span
|
||||
[libPopover]="popoverCard"
|
||||
[libPopoverPosition]="'before'"
|
||||
[libPopoverHideDelay]="100"
|
||||
[libPopoverShowDelay]="100"
|
||||
>
|
||||
<lib-urls
|
||||
*ngFor="let nb of element.notebooks"
|
||||
[urlList]="[getUrlItem(nb, element)]"
|
||||
>
|
||||
</lib-urls>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ng-template #popoverCard>
|
||||
<div class="popoverCard">
|
||||
<p>Notebooks</p>
|
||||
<li *ngFor="let nb of element.notebooks">
|
||||
<lib-urls [urlList]="[getUrlItem(nb, element)]"></lib-urls>
|
||||
</li>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.used-by-container {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.popoverCard {
|
||||
max-width: 400px;
|
||||
padding: 4px 12px 4px 4px;
|
||||
}
|
||||
|
|
@ -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<UsedByComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export interface PVCResponseObject {
|
|||
name: string;
|
||||
namespace: string;
|
||||
status: Status;
|
||||
notebooks: string[];
|
||||
}
|
||||
|
||||
export interface PVCProcessedObject extends PVCResponseObject {
|
||||
|
|
|
|||
Loading…
Reference in New Issue