jwa(front): Add LOGS tab to the notebook details page (kubeflow/kubeflow#6779)
* jwa(back): Get a notebook's logs Add logic in the backend to fetch the logs of a notebook's underlying pod. Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com> * jwa(front): Add LOGS tab to Notebook details page - Create Logs tab to show the logs of the notebook's underlying pod. - Create Logs Viewer component in Kubeflow common library exactly like the one we have in MWA. Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com> Signed-off-by: Orfeas Kourkakis <orfeas@arrikto.com>
This commit is contained in:
parent
7c9be4729a
commit
03c240a5d5
|
|
@ -10,7 +10,7 @@ def list_pods(namespace, auth=True, label_selector = None):
|
|||
|
||||
def get_pod_logs(namespace, pod, container, auth=True):
|
||||
if auth:
|
||||
authz.ensure_authorized("read", "", "v1", "pods", namespace, "logs")
|
||||
authz.ensure_authorized("get", "", "v1", "pods", namespace, "log")
|
||||
|
||||
return v1_core.read_namespaced_pod_log(
|
||||
namespace=namespace, name=pod, container=container
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ export * from './lib/logs-viewer/logs-viewer.component';
|
|||
export * from './lib/help-popover/help-popover.component';
|
||||
export * from './lib/help-popover/help-popover.module';
|
||||
|
||||
export * from './lib/logs-viewer/logs-viewer.module';
|
||||
export * from './lib/logs-viewer/logs-viewer.component';
|
||||
|
||||
export * from './lib/content-list-item/content-list-item.component';
|
||||
export * from './lib/content-list-item/content-list-item.module';
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,15 @@ def get_notebook_pod(notebook_name, namespace):
|
|||
|
||||
|
||||
|
||||
@bp.route("/api/namespaces/<namespace>/notebooks/<notebook_name>/pod/<pod_name>/logs")
|
||||
def get_pod_logs(namespace, notebook_name, pod_name):
|
||||
container = notebook_name
|
||||
logs = api.get_pod_logs(namespace, pod_name, container)
|
||||
return api.success_response(
|
||||
"logs", logs.split("\n"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@bp.route("/api/gpus")
|
||||
def get_gpu_vendors():
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<lib-loading-spinner *ngIf="logsLoading"></lib-loading-spinner>
|
||||
|
||||
<!--if no logs are present at all then show a warning message-->
|
||||
<ng-container *ngIf="!logsLoading && logsEmpty">
|
||||
<lib-panel *ngIf="!pod"> No logs were found for this Notebook. </lib-panel>
|
||||
<lib-panel *ngIf="pod" icon="error" color="warn">
|
||||
Error: {{ loadErrorMsg }}
|
||||
</lib-panel>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!logsLoading && !logsEmpty">
|
||||
<lib-logs-viewer
|
||||
class="logs-viewer"
|
||||
heading="Notebook Pod Logs"
|
||||
[subHeading]="''"
|
||||
[logs]="logs"
|
||||
></lib-logs-viewer>
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
lib-panel {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.logs-viewer {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { LoadingSpinnerModule, PollerService } from 'kubeflow';
|
||||
import { JWABackendService } from 'src/app/services/backend.service';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { LogsComponent } from './logs.component';
|
||||
const JWABackendServiceStub: Partial<JWABackendService> = {
|
||||
getPodDefaults: () => of(),
|
||||
};
|
||||
const PollerServiceStub: Partial<PollerService> = {
|
||||
exponential: () => of(),
|
||||
};
|
||||
|
||||
describe('LogsComponent', () => {
|
||||
let component: LogsComponent;
|
||||
let fixture: ComponentFixture<LogsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [LogsComponent],
|
||||
providers: [
|
||||
{ provide: JWABackendService, useValue: JWABackendServiceStub },
|
||||
{ provide: PollerService, useValue: PollerServiceStub },
|
||||
],
|
||||
imports: [LoadingSpinnerModule],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { JWABackendService } from 'src/app/services/backend.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { PollerService } from 'kubeflow';
|
||||
import { V1Pod } from '@kubernetes/client-node';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logs',
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrls: ['./logs.component.scss'],
|
||||
})
|
||||
export class LogsComponent implements OnInit, OnDestroy {
|
||||
public goToBottom = true;
|
||||
public logs: string[];
|
||||
public logsRequestCompleted = false;
|
||||
public loadErrorMsg = '';
|
||||
|
||||
private pollingSub: Subscription;
|
||||
private prvPod: V1Pod;
|
||||
|
||||
@Input() podRequestCompleted = false;
|
||||
|
||||
@Input()
|
||||
set pod(pod: V1Pod) {
|
||||
this.prvPod = pod;
|
||||
|
||||
if (!pod) {
|
||||
this.logs = null;
|
||||
this.logsRequestCompleted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.poll(pod);
|
||||
}
|
||||
get pod() {
|
||||
return this.prvPod;
|
||||
}
|
||||
|
||||
private poll(pod: V1Pod) {
|
||||
if (this.pollingSub) {
|
||||
this.pollingSub.unsubscribe();
|
||||
}
|
||||
|
||||
const request = this.backend.getPodLogs(pod);
|
||||
this.pollingSub = this.poller.exponential(request).subscribe(
|
||||
logs => {
|
||||
this.logs = logs;
|
||||
this.logsRequestCompleted = true;
|
||||
this.loadErrorMsg = '';
|
||||
},
|
||||
error => {
|
||||
this.logs = null;
|
||||
this.logsRequestCompleted = true;
|
||||
this.loadErrorMsg = error;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
get logsLoading(): boolean {
|
||||
if (!this.podRequestCompleted) {
|
||||
return true;
|
||||
} else if (this.podRequestCompleted && !this.logsRequestCompleted) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get logsEmpty(): boolean {
|
||||
return this.logs ? false : true;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public backend: JWABackendService,
|
||||
public poller: PollerService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.pollingSub) {
|
||||
this.pollingSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LogsComponent } from './logs.component';
|
||||
import {
|
||||
HeadingSubheadingRowModule,
|
||||
KubeflowModule,
|
||||
LogsViewerModule,
|
||||
} from 'kubeflow';
|
||||
|
||||
@NgModule({
|
||||
declarations: [LogsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
KubeflowModule,
|
||||
HeadingSubheadingRowModule,
|
||||
LogsViewerModule,
|
||||
],
|
||||
exports: [LogsComponent],
|
||||
})
|
||||
export class LogsModule {}
|
||||
|
|
@ -32,6 +32,16 @@
|
|||
></app-overview>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="LOGS">
|
||||
<ng-template matTabContent>
|
||||
<app-logs
|
||||
[pod]="notebookPod"
|
||||
[podRequestCompleted]="podRequestCompleted"
|
||||
></app-logs>
|
||||
</ng-template>
|
||||
</mat-tab>
|
||||
|
||||
</mat-tab-group>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { NotebookPageComponent } from './notebook-page.component';
|
||||
import { OverviewModule } from './overview/overview.module';
|
||||
import { LogsModule } from './logs/logs.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
|
|
@ -19,6 +20,7 @@ import { RouterModule } from '@angular/router';
|
|||
MatTabsModule,
|
||||
OverviewModule,
|
||||
MatProgressSpinnerModule,
|
||||
LogsModule,
|
||||
RouterModule,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -76,6 +76,17 @@ export class JWABackendService extends BackendService {
|
|||
);
|
||||
}
|
||||
|
||||
public getPodLogs(pod: V1Pod): Observable<string[]> {
|
||||
const namespace = pod.metadata.namespace;
|
||||
const notebookName = pod.metadata.labels['notebook-name'];
|
||||
const podName = pod.metadata.name;
|
||||
const url = `api/namespaces/${namespace}/notebooks/${notebookName}/pod/${podName}/logs`;
|
||||
return this.http.get<JWABackendResponse>(url).pipe(
|
||||
catchError(error => this.handleErrorExtended(error, [404, 400])),
|
||||
map((resp: JWABackendResponse) => resp.logs),
|
||||
);
|
||||
}
|
||||
|
||||
public getConfig(): Observable<Config> {
|
||||
const url = `api/config`;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { PvcResponseObject } from './volume';
|
|||
export interface JWABackendResponse extends BackendResponse {
|
||||
notebook?: NotebookRawObject;
|
||||
notebooks?: NotebookResponseObject[];
|
||||
logs: string[];
|
||||
pvcs?: PvcResponseObject[];
|
||||
config?: Config;
|
||||
poddefaults?: PodDefault[];
|
||||
|
|
|
|||
Loading…
Reference in New Issue