jwa: Add EVENTS tab to Notebook details page (kubeflow/kubeflow#6782)

* jwa(back): Get a notebook's events

 - Add logic in the backend to fetch the events of a notebook.
 - Fix the authorization parameters in the get_notebook_events
   function.

Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>

* jwa(front): Add EVENTS tab to Notebook details page

Add Events tab to show the available events of the notebook in the
form of a table, in order to help a user with debugging the
notebook.

Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>

Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>
This commit is contained in:
Orfeas Kourkakis 2022-12-01 18:25:45 +02:00 committed by GitHub
parent 03c240a5d5
commit 2a2ad29745
13 changed files with 205 additions and 0 deletions

View File

@ -55,6 +55,10 @@ def patch_notebook(notebook, namespace, body):
def list_notebook_events(notebook, namespace):
authz.ensure_authorized(
"list", "", "v1", "events", namespace
)
selector = "involvedObject.kind=Notebook,involvedObject.name=" + notebook
return v1_core.list_namespaced_event(

View File

@ -87,6 +87,14 @@ def get_pod_logs(namespace, notebook_name, pod_name):
)
@bp.route("/api/namespaces/<namespace>/notebooks/<notebook_name>/events")
def get_notebook_events(notebook_name, namespace):
events = api.list_notebook_events(notebook_name, namespace).items
return api.success_response(
"events", api.serialize(events),
)
@bp.route("/api/gpus")
def get_gpu_vendors():

View File

@ -0,0 +1,47 @@
import { PropertyValue, TableConfig, DateTimeValue } from 'kubeflow';
// --- Config for the Resource Table ---
export const defaultConfig: TableConfig = {
dynamicNamespaceColumn: true,
columns: [
{
matHeaderCellDef: $localize`Type`,
matColumnDef: 'type',
style: { width: '12%' },
value: new PropertyValue({
field: 'type',
tooltipField: 'type',
truncate: true,
}),
sort: true,
},
{
matHeaderCellDef: $localize`Reason`,
matColumnDef: 'reason',
style: { width: '12%' },
value: new PropertyValue({
field: 'reason',
tooltipField: 'reason',
truncate: true,
}),
sort: true,
},
{
matHeaderCellDef: $localize`Created at`,
matColumnDef: 'age',
style: { width: '12%' },
value: new DateTimeValue({ field: 'metadata.creationTimestamp' }),
sort: true,
},
{
matHeaderCellDef: $localize`Message`,
matColumnDef: 'message',
value: new PropertyValue({
field: 'message',
tooltipField: 'message',
truncate: true,
}),
sort: true,
},
],
};

View File

@ -0,0 +1,3 @@
<div class="page-padding lib-flex-grow lib-overflow-auto">
<lib-table [config]="config" [data]="processedEvents"></lib-table>
</div>

View File

@ -0,0 +1,38 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KubeflowModule, PollerService } from 'kubeflow';
import { JWABackendService } from 'src/app/services/backend.service';
import { of } from 'rxjs';
import { EventsComponent } from './events.component';
const JWABackendServiceStub: Partial<JWABackendService> = {
getPodDefaults: () => of(),
};
const PollerServiceStub: Partial<PollerService> = {
exponential: () => of(),
};
describe('EventsComponent', () => {
let component: EventsComponent;
let fixture: ComponentFixture<EventsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [EventsComponent],
imports: [KubeflowModule],
providers: [
{ provide: JWABackendService, useValue: JWABackendServiceStub },
{ provide: PollerService, useValue: PollerServiceStub },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(EventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,65 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { PollerService } from 'kubeflow';
import { Subscription } from 'rxjs';
import { JWABackendService } from 'src/app/services/backend.service';
import { NotebookRawObject } from 'src/app/types';
import { EventObject } from 'src/app/types/event';
import { defaultConfig } from './config';
import { isEqual } from 'lodash-es';
@Component({
selector: 'app-events',
templateUrl: './events.component.html',
styleUrls: ['./events.component.scss'],
})
export class EventsComponent implements OnInit, OnDestroy {
private events: EventObject[] = [];
public processedEvents: EventObject[] = [];
public config = defaultConfig;
private pollSub = new Subscription();
private prvNotebook: NotebookRawObject;
@Input()
set notebook(nb: NotebookRawObject) {
this.prvNotebook = nb;
this.poll(nb);
}
get notebook(): NotebookRawObject {
return this.prvNotebook;
}
constructor(
public backend: JWABackendService,
public poller: PollerService,
) {}
ngOnInit(): void {}
ngOnDestroy(): void {
if (this.pollSub) {
this.pollSub.unsubscribe();
}
}
private poll(notebook: NotebookRawObject) {
this.pollSub.unsubscribe();
const request = this.backend.getNotebookEvents(notebook);
this.pollSub = this.poller.exponential(request).subscribe(events => {
this.events = events;
this.processEvents(events);
});
}
processEvents(events: EventObject[]) {
const eventsCopy = Array.from(events);
for (const event of eventsCopy) {
event.message = event.message.replace('Reissued from ', '');
}
if (isEqual(eventsCopy, this.processedEvents)) {
return;
}
this.processedEvents = eventsCopy;
}
}

View File

@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EventsComponent } from './events.component';
import { KubeflowModule } from 'kubeflow';
@NgModule({
declarations: [EventsComponent],
imports: [CommonModule, KubeflowModule],
exports: [EventsComponent],
})
export class EventsModule {}

View File

@ -42,6 +42,12 @@
</ng-template>
</mat-tab>
<mat-tab label="EVENTS">
<ng-template matTabContent>
<app-events [notebook]="notebook"></app-events>
</ng-template>
</mat-tab>
</mat-tab-group>
</ng-container>
</div>

View File

@ -9,6 +9,7 @@ import { NotebookPageComponent } from './notebook-page.component';
import { OverviewModule } from './overview/overview.module';
import { LogsModule } from './logs/logs.module';
import { RouterModule } from '@angular/router';
import { EventsModule } from './events/events.module';
@NgModule({
declarations: [NotebookPageComponent],
@ -22,6 +23,7 @@ import { RouterModule } from '@angular/router';
MatProgressSpinnerModule,
LogsModule,
RouterModule,
EventsModule,
],
})
export class NotebookPageModule {}

View File

@ -14,6 +14,7 @@ import {
PvcResponseObject,
} from '../types';
import { V1Pod } from '@kubernetes/client-node';
import { EventObject } from '../types/event';
@Injectable({
providedIn: 'root',
})
@ -87,6 +88,19 @@ export class JWABackendService extends BackendService {
);
}
public getNotebookEvents(
notebook: NotebookRawObject,
): Observable<EventObject[]> {
const namespace = notebook.metadata.namespace;
const notebookName = notebook.metadata.name;
const url = `api/namespaces/${namespace}/notebooks/${notebookName}/events`;
return this.http.get<JWABackendResponse>(url).pipe(
catchError(error => this.handleErrorExtended(error, [404])),
map((resp: JWABackendResponse) => resp.events),
);
}
public getConfig(): Observable<Config> {
const url = `api/config`;

View File

@ -0,0 +1,5 @@
import { EventsV1Event } from '@kubernetes/client-node';
export interface EventObject extends EventsV1Event {
message?: string;
}

View File

@ -1,6 +1,7 @@
import { V1Pod } from '@kubernetes/client-node';
import { BackendResponse } from 'kubeflow';
import { Config } from './config';
import { EventObject } from './event';
import { NotebookRawObject, NotebookResponseObject } from './notebook';
import { PodDefault } from './poddefault';
import { PvcResponseObject } from './volume';
@ -14,4 +15,5 @@ export interface JWABackendResponse extends BackendResponse {
poddefaults?: PodDefault[];
vendors?: string[];
pod?: V1Pod;
events?: EventObject[];
}