Skip to content

Commit

Permalink
Update project list view to readily show active notebooks (#3348)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 authored Oct 18, 2024
1 parent 3b5a56b commit 6b6dc43
Show file tree
Hide file tree
Showing 17 changed files with 394 additions and 194 deletions.
16 changes: 16 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ class ProjectNotebookRow extends TableRow {
findNotebookStatusText() {
return this.find().findByTestId('notebook-status-text');
}

findNotebookStart() {
return this.find().findByTestId('notebook-start-action');
}

findNotebookStop() {
return this.find().findByTestId('notebook-stop-action');
}
}

class ProjectRow extends TableRow {
Expand All @@ -57,10 +65,18 @@ class ProjectRow extends TableRow {
return this.find().findByTestId('notebook-column-expand');
}

findNotebookColumnExpander() {
return this.find().findByTestId('notebook-column-count');
}

findNotebookTable() {
return this.find().parents('tbody').findByTestId('project-notebooks-table');
}

getNotebookRows() {
return this.findNotebookTable().findByTestId('project-notebooks-table-row');
}

getNotebookRow(notebookName: string) {
return new ProjectNotebookRow(() => this.findNotebookLink(notebookName).parents('tr'));
}
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ class NotebookRow extends TableRow {
return this.find().findByTestId('notebook-status-text');
}

findNotebookStart() {
return this.find().findByTestId('notebook-start-action');
}

findNotebookStop() {
return this.find().findByTestId('notebook-stop-action');
}

findNotebookStatusPopover(name: string) {
return cy.findByTestId('notebook-status-popover').contains(name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,9 @@ describe('Data science projects details', () => {
);
projectListPage.visit();
const projectTableRow = projectListPage.getProjectRow('Test Project');
projectTableRow.findNotebookColumn().click();
cy.wait('@getWorkbench');
projectTableRow.findNotebookColumnExpander().click();
const notebookRows = projectTableRow.getNotebookRows();
notebookRows.should('have.length', 1);
});

it('should open the modal to stop workbench when user stops the workbench', () => {
Expand Down Expand Up @@ -316,13 +317,14 @@ describe('Data science projects details', () => {
);
projectListPage.visit();
const projectTableRow = projectListPage.getProjectRow('Test Project');
projectTableRow.findNotebookColumn().click();
projectTableRow.findNotebookColumnExpander().click();
const notebookRows = projectTableRow.getNotebookRows();
notebookRows.should('have.length', 1);

const notebookRow = projectTableRow.getNotebookRow('Test Notebook');
notebookRow.findNotebookRouteLink().should('have.attr', 'aria-disabled', 'false');

notebookRow.findKebabAction('Start').should('be.disabled');
notebookRow.findKebabAction('Stop').click();
notebookRow.findNotebookStop().click();

//stop workbench
notebookConfirmModal.findStopWorkbenchButton().should('be.enabled');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ describe('Workbench page', () => {
const notebookRow = workbenchPage.getNotebookRow('Test Notebook');

//stop Workbench
notebookRow.findKebabAction('Stop').click();
notebookRow.findNotebookStop().click();
notebookConfirmModal.findStopWorkbenchButton().should('be.enabled');
cy.interceptK8s(
NotebookModel,
Expand Down Expand Up @@ -584,7 +584,7 @@ describe('Workbench page', () => {
}),
);

notebookRow.findKebabAction('Start').click();
notebookRow.findNotebookStart().click();
notebookRow.findHaveNotebookStatusText().should('have.text', 'Starting');
notebookRow.findHaveNotebookStatusText().click();

Expand Down
162 changes: 31 additions & 131 deletions frontend/src/pages/projects/notebook/NotebookActionsColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,140 +3,40 @@ import { ActionsColumn } from '@patternfly/react-table';
import { useNavigate } from 'react-router-dom';
import { NotebookKind, ProjectKind } from '~/k8sTypes';
import { NotebookState } from '~/pages/projects/notebook/types';
import { fireFormTrackingEvent } from '~/concepts/analyticsTracking/segmentIOUtils';
import { TrackingOutcome } from '~/concepts/analyticsTracking/trackingProperties';
import { startNotebook, stopNotebook } from '~/api';
import useNotebookAcceleratorProfile from '~/pages/projects/screens/detail/notebooks/useNotebookAcceleratorProfile';
import useNotebookDeploymentSize from '~/pages/projects/screens/detail/notebooks/useNotebookDeploymentSize';
import useStopNotebookModalAvailability from '~/pages/projects/notebook/useStopNotebookModalAvailability';
import { useAppContext } from '~/app/AppContext';
import { computeNotebooksTolerations } from '~/utilities/tolerations';
import { currentlyHasPipelines } from '~/concepts/pipelines/elyra/utils';
import StopNotebookConfirmModal from '~/pages/projects/notebook/StopNotebookConfirmModal';
import useNotebookImage from '~/pages/projects/screens/detail/notebooks/useNotebookImage';
import { NotebookImageAvailability } from '~/pages/projects/screens/detail/notebooks/const';

export const useNotebookActionsColumn = (
project: ProjectKind,
notebookState: NotebookState,
enablePipelines: boolean,
onNotebookDelete: (notebook: NotebookKind) => void,
): [React.ReactNode, () => void] => {
const navigate = useNavigate();
const { notebook, isStarting, isRunning, isStopping, refresh } = notebookState;
const acceleratorProfile = useNotebookAcceleratorProfile(notebook);
const { size } = useNotebookDeploymentSize(notebook);
const [isOpenConfirm, setOpenConfirm] = React.useState(false);
const [inProgress, setInProgress] = React.useState(false);
const [notebookImage] = useNotebookImage(notebookState.notebook);
const [dontShowModalValue] = useStopNotebookModalAvailability();
const { dashboardConfig } = useAppContext();
const notebookName = notebook.metadata.name;
const notebookNamespace = notebook.metadata.namespace;
const isDisabled =
isStopping ||
inProgress ||
(notebookImage?.imageAvailability === NotebookImageAvailability.DELETED && !isRunning);
const isRunningOrStarting = isStarting || isRunning;

const fireNotebookTrackingEvent = React.useCallback(
(action: 'started' | 'stopped') => {
fireFormTrackingEvent(`Workbench ${action === 'started' ? 'Started' : 'Stopped'}`, {
outcome: TrackingOutcome.submit,
acceleratorCount: acceleratorProfile.unknownProfileDetected
? undefined
: acceleratorProfile.count,
accelerator: acceleratorProfile.acceleratorProfile
? `${acceleratorProfile.acceleratorProfile.spec.displayName} (${acceleratorProfile.acceleratorProfile.metadata.name}): ${acceleratorProfile.acceleratorProfile.spec.identifier}`
: acceleratorProfile.unknownProfileDetected
? 'Unknown'
: 'None',
lastSelectedSize:
size?.name ||
notebook.metadata.annotations?.['notebooks.opendatahub.io/last-size-selection'],
lastSelectedImage:
notebook.metadata.annotations?.['notebooks.opendatahub.io/last-image-selection'],
projectName: notebook.metadata.namespace,
notebookName: notebook.metadata.name,
...(action === 'stopped' && {
lastActivity: notebook.metadata.annotations?.['notebooks.kubeflow.org/last-activity'],
}),
});
},
[acceleratorProfile, notebook, size],
);
type Props = {
project: ProjectKind;
notebookState: NotebookState;
onNotebookDelete: (notebook: NotebookKind) => void;
};

const handleStop = React.useCallback(() => {
fireNotebookTrackingEvent('stopped');
setInProgress(true);
stopNotebook(notebookName, notebookNamespace).then(() => {
refresh().then(() => setInProgress(false));
});
}, [fireNotebookTrackingEvent, notebookName, notebookNamespace, refresh]);
export const NotebookActionsColumn: React.FC<Props> = ({
project,
notebookState,
onNotebookDelete,
}) => {
const navigate = useNavigate();
const { isStarting, isStopping } = notebookState;

return [
<>
<ActionsColumn
items={[
{
isDisabled: isDisabled || isRunningOrStarting,
title: 'Start',
onClick: () => {
setInProgress(true);
const tolerationSettings = computeNotebooksTolerations(
dashboardConfig,
notebookState.notebook,
);
startNotebook(
notebook,
tolerationSettings,
enablePipelines && !currentlyHasPipelines(notebook),
).then(() => {
fireNotebookTrackingEvent('started');
refresh().then(() => setInProgress(false));
});
},
},
{
isDisabled: isDisabled || !isRunningOrStarting,
title: 'Stop',
onClick: () => {
if (dontShowModalValue) {
handleStop();
} else {
setOpenConfirm(true);
}
},
return (
<ActionsColumn
items={[
{
isDisabled: isStarting || isStopping,
title: 'Edit workbench',
onClick: () => {
navigate(
`/projects/${project.metadata.name}/spawner/${notebookState.notebook.metadata.name}`,
);
},
{
isDisabled: isStarting || isStopping,
title: 'Edit workbench',
onClick: () => {
navigate(
`/projects/${project.metadata.name}/spawner/${notebookState.notebook.metadata.name}`,
);
},
},
{
title: 'Delete workbench',
onClick: () => {
onNotebookDelete(notebookState.notebook);
},
{
title: 'Delete workbench',
onClick: () => {
onNotebookDelete(notebookState.notebook);
},
},
]}
/>
{isOpenConfirm ? (
<StopNotebookConfirmModal
notebookState={notebookState}
onClose={(confirmStatus) => {
if (confirmStatus) {
handleStop();
}
setOpenConfirm(false);
}}
/>
) : null}
</>,
handleStop,
];
},
]}
/>
);
};
46 changes: 46 additions & 0 deletions frontend/src/pages/projects/notebook/NotebookStateAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Button } from '@patternfly/react-core';
import { NotebookImageAvailability } from '~/pages/projects/screens/detail/notebooks/const';
import useNotebookImage from '~/pages/projects/screens/detail/notebooks/useNotebookImage';
import { NotebookState } from './types';

type Props = {
notebookState: NotebookState;
onStart: () => void;
onStop: () => void;
isDisabled?: boolean;
};

const NotebookStateAction: React.FC<Props> = ({ notebookState, onStart, onStop, isDisabled }) => {
const { notebook, isStarting, isRunning, isStopping } = notebookState;
const [notebookImage] = useNotebookImage(notebook);

const actionDisabled =
isDisabled ||
isStopping ||
(notebookImage?.imageAvailability === NotebookImageAvailability.DELETED && !isRunning);

return isStarting || isRunning ? (
<Button
data-testid="notebook-stop-action"
variant="link"
isInline
isDisabled={actionDisabled}
onClick={onStop}
>
Stop
</Button>
) : (
<Button
data-testid="notebook-start-action"
variant="link"
isInline
isDisabled={actionDisabled}
onClick={onStart}
>
Start
</Button>
);
};

export default NotebookStateAction;
8 changes: 4 additions & 4 deletions frontend/src/pages/projects/notebook/NotebookStateStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import { Button, Label, LabelProps, Popover, Tooltip } from '@patternfly/react-core';
import {
BanIcon,
ExclamationCircleIcon,
InProgressIcon,
RunningIcon,
OffIcon,
PlayIcon,
SyncAltIcon,
} from '@patternfly/react-icons';
import { EventStatus } from '~/types';
Expand Down Expand Up @@ -52,9 +52,9 @@ const NotebookStateStatus: React.FC<NotebookStateStatusProps> = ({
};
}
if (isRunning) {
return { label: 'Running', color: 'green', icon: <RunningIcon /> };
return { label: 'Running', color: 'green', icon: <PlayIcon /> };
}
return { label: 'Stopped', color: 'grey', icon: <BanIcon /> };
return { label: 'Stopped', color: 'grey', icon: <OffIcon /> };
}, [isError, isRunning, isStarting, isStopping]);

const StatusLabel = (
Expand Down
33 changes: 32 additions & 1 deletion frontend/src/pages/projects/notebook/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { EventKind, NotebookKind } from '~/k8sTypes';
import { EventStatus, NotebookStatus } from '~/types';
import { EventStatus, NotebookSize, NotebookStatus } from '~/types';
import { ROOT_MOUNT_PATH } from '~/pages/projects/pvc/const';
import { fireFormTrackingEvent } from '~/concepts/analyticsTracking/segmentIOUtils';
import { TrackingOutcome } from '~/concepts/analyticsTracking/trackingProperties';
import { AcceleratorProfileState } from '~/utilities/useAcceleratorProfileState';
import { useWatchNotebookEvents } from './useWatchNotebookEvents';

export const hasStopAnnotation = (notebook: NotebookKind): boolean =>
Expand Down Expand Up @@ -264,3 +267,31 @@ export const useNotebookStatus = (

export const getEventFullMessage = (event: EventKind): string =>
`${getEventTimestamp(event)} [${event.type}] ${event.message}`;

export const fireNotebookTrackingEvent = (
action: 'started' | 'stopped',
notebook: NotebookKind,
size: NotebookSize | null,
acceleratorProfile: AcceleratorProfileState,
): void => {
fireFormTrackingEvent(`Workbench ${action === 'started' ? 'Started' : 'Stopped'}`, {
outcome: TrackingOutcome.submit,
acceleratorCount: acceleratorProfile.unknownProfileDetected
? undefined
: acceleratorProfile.count,
accelerator: acceleratorProfile.acceleratorProfile
? `${acceleratorProfile.acceleratorProfile.spec.displayName} (${acceleratorProfile.acceleratorProfile.metadata.name}): ${acceleratorProfile.acceleratorProfile.spec.identifier}`
: acceleratorProfile.unknownProfileDetected
? 'Unknown'
: 'None',
lastSelectedSize:
size?.name || notebook.metadata.annotations?.['notebooks.opendatahub.io/last-size-selection'],
lastSelectedImage:
notebook.metadata.annotations?.['notebooks.opendatahub.io/last-image-selection'],
projectName: notebook.metadata.namespace,
notebookName: notebook.metadata.name,
...(action === 'stopped' && {
lastActivity: notebook.metadata.annotations?.['notebooks.kubeflow.org/last-activity'],
}),
});
};
Loading

0 comments on commit 6b6dc43

Please sign in to comment.