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:
parent
03c240a5d5
commit
2a2ad29745
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<div class="page-padding lib-flex-grow lib-overflow-auto">
|
||||
<lib-table [config]="config" [data]="processedEvents"></lib-table>
|
||||
</div>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
import { EventsV1Event } from '@kubernetes/client-node';
|
||||
|
||||
export interface EventObject extends EventsV1Event {
|
||||
message?: string;
|
||||
}
|
||||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue