diff --git a/backend/src/types.ts b/backend/src/types.ts index 474ddfed8c..020c118a54 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -29,7 +29,7 @@ export type DashboardConfig = K8sResourceCommon & { disableProjectSharing: boolean; disableCustomServingRuntimes: boolean; disablePipelines: boolean; - disableBiasMetrics: boolean; + disableTrustyBiasMetrics: boolean; disablePerformanceMetrics: boolean; disableKServe: boolean; disableKServeAuth: boolean; diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 70d35a9c52..7921c96692 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -53,7 +53,7 @@ export const blankDashboardCR: DashboardConfig = { disableModelServing: false, disableProjectSharing: false, disableCustomServingRuntimes: false, - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disablePipelines: false, disableKServe: false, diff --git a/backend/src/utils/resourceUtils.ts b/backend/src/utils/resourceUtils.ts index 2c2e5a20ce..c65703547e 100644 --- a/backend/src/utils/resourceUtils.ts +++ b/backend/src/utils/resourceUtils.ts @@ -76,9 +76,7 @@ const DASHBOARD_CONFIG = { }; const fetchDashboardCR = async (fastify: KubeFastifyInstance): Promise => { - return fetchOrCreateDashboardCR(fastify) - .then(softDisableBiasMetrics(fastify)) - .then((dashboardCR) => [dashboardCR]); + return fetchOrCreateDashboardCR(fastify).then((dashboardCR) => [dashboardCR]); }; const fetchWatchedClusterStatus = async ( @@ -87,48 +85,6 @@ const fetchWatchedClusterStatus = async ( return fetchClusterStatus(fastify).then((clusterStatus) => [clusterStatus]); }; -/** - * TODO: Support Bias Metrics https://issues.redhat.com/browse/RHOAIENG-13084 - * For RHOAI Only - * Always disable bias metrics until we can properly support the new UI flow. - * Note: This does no changes to the on-cluster value -- there can be a de-sync - */ -const softDisableBiasMetrics = - (fastify: KubeFastifyInstance) => - (dashboardConfig: DashboardConfig): DashboardConfig & { status?: object } => { - if (!isRHOAI(fastify)) { - return dashboardConfig; - } - - fastify.log.info( - 'Trusty Bias Metrics are explicitly disabled in the cached odh-dashboard-config', - ); - return { - ...dashboardConfig, - spec: { - ...dashboardConfig.spec, - dashboardConfig: { - ...dashboardConfig.spec.dashboardConfig, - disableBiasMetrics: true, - }, - }, - // NOTE: This is a fake status to help show in the UI that we are overriding the value - // The CRD does not support a status property today and should not have existing statuses here - // No code should use this -- this is purely for debugging in the UI - // To be removed as soon as we can support trusty properly - status: { - conditions: [ - { - message: 'This feature flag state is being ignored', - reason: 'IgnoredFeatureFlag', - status: 'False', - type: 'disableBiasMetricsAvailable', - }, - ], - }, - }; - }; - const fetchOrCreateDashboardCR = async (fastify: KubeFastifyInstance): Promise => { return fastify.kube.customObjectsApi .getNamespacedCustomObject( diff --git a/docs/dashboard-config.md b/docs/dashboard-config.md index 83e3e88756..a77491dd56 100644 --- a/docs/dashboard-config.md +++ b/docs/dashboard-config.md @@ -30,7 +30,7 @@ The following are a list of features that are supported, along with there defaul | disableKServeMetrics | false | Disables the ability to see KServe Metrics. | | disableModelMesh | false | Disables the ability to select ModelMesh as a Serving Platform. | | disableAcceleratorProfiles | false | Disables Accelerator profiles from the Admin Panel. | -| disableBiasMetrics | false | Disables Model Bias tab from Model Serving metrics. | +| disableTrustyBiasMetrics | false | Disables Model Bias tab from Model Serving metrics. | | disablePerformanceMetrics | false | Disables Endpoint Performance tab from Model Serving metrics. | | disableDistributedWorkloads | false | Disables Distributed Workload Metrics from the dashboard. | | disableModelRegistry | false | Disables Model Registry from the dashboard. | @@ -62,7 +62,7 @@ spec: disableCustomServingRuntimes: false disableAcceleratorProfiles: false disableKServeMetrics: false - disableBiasMetrics: false + disableTrustyBiasMetrics: false disablePerformanceMetrics: false disableDistributedWorkloads: false disableConnectionTypes: false @@ -159,7 +159,7 @@ spec: disableCustomServingRuntimes: false disableAcceleratorProfiles: true disableKServeMetrics: true - disableBiasMetrics: false + disableTrustyBiasMetrics: false disablePerformanceMetrics: false disableNIMModelServing: true notebookController: diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts index 5096817893..b9e2bd1b75 100644 --- a/frontend/src/__mocks__/mockDashboardConfig.ts +++ b/frontend/src/__mocks__/mockDashboardConfig.ts @@ -21,7 +21,7 @@ type MockDashboardConfigType = { disableModelMesh?: boolean; disableAcceleratorProfiles?: boolean; disablePerformanceMetrics?: boolean; - disableBiasMetrics?: boolean; + disableTrustyBiasMetrics?: boolean; disableDistributedWorkloads?: boolean; disableModelRegistry?: boolean; disableConnectionTypes?: boolean; @@ -51,7 +51,7 @@ export const mockDashboardConfig = ({ disableModelMesh = false, disableAcceleratorProfiles = false, disablePerformanceMetrics = false, - disableBiasMetrics = false, + disableTrustyBiasMetrics = false, disableDistributedWorkloads = false, disableModelRegistry = false, disableConnectionTypes = true, @@ -152,7 +152,7 @@ export const mockDashboardConfig = ({ disableCustomServingRuntimes, disablePipelines, disableProjectSharing: false, - disableBiasMetrics, + disableTrustyBiasMetrics, disablePerformanceMetrics, disableKServe, disableKServeAuth, diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index 733e8e597c..8e8b63cf20 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -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 { @@ -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')); } diff --git a/frontend/src/__tests__/cypress/cypress/pages/workbench.ts b/frontend/src/__tests__/cypress/cypress/pages/workbench.ts index d26a380023..b4b771b97c 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/workbench.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/workbench.ts @@ -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); } diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts index 3f4d3d7d0d..586070c0a8 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistrySettings/modelRegistryPermissions.cy.ts @@ -396,8 +396,26 @@ describe('MR Permissions', () => { it('Disabled actions on default group', () => { initIntercepts({ isEmpty: false }); modelRegistryPermissions.visit('example-mr'); - groupTable.getTableRow('example-mr-users').findKebab().should('be.disabled'); - groupTable.getTableRow('example-mr-users-2').findKebab().should('not.be.disabled'); + + cy.contains('td', 'example-mr-users') + .closest('tr') + .within(() => { + cy.get('button[aria-disabled="true"]') + .should('exist') + .and( + 'have.attr', + 'aria-label', + 'The default group always has access to model registry.', + ) + .find('svg') + .should('exist'); + }); + + cy.contains('td', 'example-mr-users-2') + .closest('tr') + .within(() => { + cy.get('button:not([aria-disabled="true"])').should('exist').and('be.visible'); + }); }); }); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelMetrics.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelMetrics.cy.ts index 39c34205ad..dcd1784093 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelMetrics.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelMetrics.cy.ts @@ -53,7 +53,7 @@ import { type HandlersProps = { disablePerformanceMetrics?: boolean; - disableBiasMetrics?: boolean; + disableTrustyBiasMetrics?: boolean; disableKServeMetrics?: boolean; servingRuntimes?: ServingRuntimeKind[]; inferenceServices?: InferenceServiceKind[]; @@ -81,7 +81,7 @@ const mockTrustyDBSecret = (): SecretKind => const initIntercepts = ({ disablePerformanceMetrics, - disableBiasMetrics, + disableTrustyBiasMetrics, disableKServeMetrics, servingRuntimes = [mockServingRuntimeK8sResource({})], inferenceServices = [mockInferenceServiceK8sResource({ isModelMesh: true })], @@ -100,7 +100,7 @@ const initIntercepts = ({ cy.interceptOdh( 'GET /api/config', mockDashboardConfig({ - disableBiasMetrics, + disableTrustyBiasMetrics, disablePerformanceMetrics, disableKServeMetrics, }), @@ -210,7 +210,7 @@ const initIntercepts = ({ describe('Model Metrics', () => { it('Empty State No Serving Data Available', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -222,7 +222,7 @@ describe('Model Metrics', () => { it('Serving Chart Shows Data', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: true, hasBiasData: false, @@ -234,7 +234,7 @@ describe('Model Metrics', () => { it('Empty State No Bias Data Available', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -264,7 +264,7 @@ describe('Model Metrics', () => { it('Bias Charts Show Data', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: true, @@ -294,7 +294,7 @@ describe('Model Metrics', () => { it('Server metrics show no data available', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -309,7 +309,7 @@ describe('Model Metrics', () => { it('Server metrics show data', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: true, hasBiasData: false, @@ -324,7 +324,7 @@ describe('Model Metrics', () => { it('Bias metrics is not configured', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -339,7 +339,7 @@ describe('Model Metrics', () => { it('Performance Metrics Tab Hidden', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: true, hasServingData: false, hasBiasData: false, @@ -351,7 +351,7 @@ describe('Model Metrics', () => { it('Bias Metrics Tab Hidden', () => { initIntercepts({ - disableBiasMetrics: true, + disableTrustyBiasMetrics: true, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -363,7 +363,7 @@ describe('Model Metrics', () => { it('Disable Trusty AI', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -393,7 +393,7 @@ describe('Model Metrics', () => { it('Enable Trusty AI', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -461,7 +461,7 @@ describe('Model Metrics', () => { it('Trusty AI enable service error', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: false, @@ -510,7 +510,7 @@ describe('Model Metrics', () => { it('Bias Metrics Show In Table', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: true, @@ -537,7 +537,7 @@ describe('Model Metrics', () => { it('Configure Bias Metric', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, hasServingData: false, hasBiasData: true, @@ -603,7 +603,7 @@ describe('Model Metrics', () => { describe('KServe performance metrics', () => { it('should inform user when area disabled', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: true, hasServingData: false, @@ -616,7 +616,7 @@ describe('KServe performance metrics', () => { it('should show error when ConfigMap is missing', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: true, @@ -639,7 +639,7 @@ describe('KServe performance metrics', () => { it('should inform user when serving runtime is unsupported', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: true, @@ -655,7 +655,7 @@ describe('KServe performance metrics', () => { it('should handle a malformed graph definition gracefully', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: true, @@ -674,7 +674,7 @@ describe('KServe performance metrics', () => { it('should display only 2 graphs, when the config specifies', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: true, @@ -695,7 +695,7 @@ describe('KServe performance metrics', () => { it('charts should not error out if a query is missing and there is other data', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: true, @@ -718,7 +718,7 @@ describe('KServe performance metrics', () => { it('charts should not error out if a query is missing and there is no data', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: false, @@ -741,7 +741,7 @@ describe('KServe performance metrics', () => { it('charts should show data when serving data is available', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: true, @@ -761,7 +761,7 @@ describe('KServe performance metrics', () => { it('charts should show empty state when no serving data is available', () => { initIntercepts({ - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServeMetrics: false, hasServingData: false, diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts index 605c917649..044a9cf5df 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts @@ -135,7 +135,7 @@ describe('ClusterStorage', () => { addClusterStorageModal.findPVSizeInput().should('have.value', '19'); addClusterStorageModal.findPVSizePlusButton().click(); addClusterStorageModal.findPVSizeInput().should('have.value', '20'); - addClusterStorageModal.selectPVSize('Mi'); + addClusterStorageModal.selectPVSize('MiB'); //connect workbench addClusterStorageModal @@ -386,7 +386,7 @@ describe('ClusterStorage', () => { clusterStorageRow.findKebabAction('Edit storage').click(); updateClusterStorageModal.findNameInput().should('have.value', 'Test Storage'); updateClusterStorageModal.findPVSizeInput().should('have.value', '5'); - updateClusterStorageModal.shouldHavePVSizeSelectValue('Gi'); + updateClusterStorageModal.shouldHavePVSizeSelectValue('GiB'); updateClusterStorageModal.findPersistentStorageWarning().should('exist'); updateClusterStorageModal.findSubmitButton().should('be.enabled'); updateClusterStorageModal.findNameInput().fill('test-updated'); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/projectList.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/projectList.cy.ts index e12b6a760b..7a47a30687 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/projectList.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/projectList.cy.ts @@ -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', () => { @@ -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'); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/workbench.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/workbench.cy.ts index 5d81eab73e..ee06548f5f 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/workbench.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/workbench.cy.ts @@ -286,7 +286,7 @@ describe('Workbench page', () => { createSpawnerPage.findPVSizeInput().should('have.value', '19'); createSpawnerPage.findPVSizePlusButton().click(); createSpawnerPage.findPVSizeInput().should('have.value', '20'); - createSpawnerPage.selectPVSize('Mi'); + createSpawnerPage.selectPVSize('MiB'); //add existing cluster storage createSpawnerPage.findExistingStorageRadio().click(); @@ -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, @@ -584,7 +584,7 @@ describe('Workbench page', () => { }), ); - notebookRow.findKebabAction('Start').click(); + notebookRow.findNotebookStart().click(); notebookRow.findHaveNotebookStatusText().should('have.text', 'Starting'); notebookRow.findHaveNotebookStatusText().click(); diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts index 61c7ce27ae..2a98ab0ff6 100644 --- a/frontend/src/concepts/areas/const.ts +++ b/frontend/src/concepts/areas/const.ts @@ -17,7 +17,7 @@ export const allFeatureFlags: string[] = Object.keys({ disableProjectSharing: false, disableCustomServingRuntimes: false, disablePipelines: false, - disableBiasMetrics: false, + disableTrustyBiasMetrics: false, disablePerformanceMetrics: false, disableKServe: false, disableKServeAuth: false, @@ -94,7 +94,7 @@ export const SupportedAreasStateMap: SupportedAreasState = { reliantAreas: [SupportedArea.DS_PROJECTS_VIEW], }, [SupportedArea.BIAS_METRICS]: { - featureFlags: ['disableBiasMetrics'], + featureFlags: ['disableTrustyBiasMetrics'], requiredComponents: [StackComponent.TRUSTY_AI], reliantAreas: [SupportedArea.MODEL_SERVING], }, diff --git a/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx index c58e6dea53..dd37431f10 100644 --- a/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx +++ b/frontend/src/concepts/roleBinding/RoleBindingPermissionsTableRow.tsx @@ -10,7 +10,12 @@ import { TimestampTooltipVariant, Tooltip, } from '@patternfly/react-core'; -import { CheckIcon, OutlinedQuestionCircleIcon, TimesIcon } from '@patternfly/react-icons'; +import { + CheckIcon, + OutlinedQuestionCircleIcon, + TimesIcon, + EllipsisVIcon, +} from '@patternfly/react-icons'; import { ProjectKind, RoleBindingKind, RoleBindingSubject } from '~/k8sTypes'; import { relativeTime } from '~/utilities/time'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; @@ -173,9 +178,18 @@ const RoleBindingPermissionsTableRow: React.FC + ) : isDefaultGroup ? ( + + + ) : ( + ); diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx index f03ed83053..5365c08523 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx @@ -54,7 +54,7 @@ const InferenceServiceEndpoint: React.FC = ({ } > ); @@ -104,7 +104,7 @@ const InferenceServiceEndpoint: React.FC = ({ } > ); diff --git a/frontend/src/pages/projects/components/StorageSizeBars.tsx b/frontend/src/pages/projects/components/StorageSizeBars.tsx index 3402111567..4a4e6dd199 100644 --- a/frontend/src/pages/projects/components/StorageSizeBars.tsx +++ b/frontend/src/pages/projects/components/StorageSizeBars.tsx @@ -33,7 +33,7 @@ const StorageSizeBar: React.FC = ({ pvc }) => { ); } - const inUseValue = `${bytesAsRoundedGiB(inUseInBytes)}Gi`; + const inUseValue = `${bytesAsRoundedGiB(inUseInBytes)}GiB`; const percentage = ((parseFloat(inUseValue) / parseFloat(maxValue)) * 100).toFixed(2); const percentageLabel = error ? '' : `Storage is ${percentage}% full`; diff --git a/frontend/src/pages/projects/notebook/NotebookActionsColumn.tsx b/frontend/src/pages/projects/notebook/NotebookActionsColumn.tsx index 9e534d68a1..fe7e1c724b 100644 --- a/frontend/src/pages/projects/notebook/NotebookActionsColumn.tsx +++ b/frontend/src/pages/projects/notebook/NotebookActionsColumn.tsx @@ -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 = ({ + project, + notebookState, + onNotebookDelete, +}) => { + const navigate = useNavigate(); + const { isStarting, isStopping } = notebookState; - return [ - <> - { - 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 ( + { + 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 ? ( - { - if (confirmStatus) { - handleStop(); - } - setOpenConfirm(false); - }} - /> - ) : null} - , - handleStop, - ]; + }, + ]} + /> + ); }; diff --git a/frontend/src/pages/projects/notebook/NotebookStateAction.tsx b/frontend/src/pages/projects/notebook/NotebookStateAction.tsx new file mode 100644 index 0000000000..cc50dbb411 --- /dev/null +++ b/frontend/src/pages/projects/notebook/NotebookStateAction.tsx @@ -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 = ({ 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 ? ( + + ) : ( + + ); +}; + +export default NotebookStateAction; diff --git a/frontend/src/pages/projects/notebook/NotebookStateStatus.tsx b/frontend/src/pages/projects/notebook/NotebookStateStatus.tsx index c25d4363e5..43aa1f199b 100644 --- a/frontend/src/pages/projects/notebook/NotebookStateStatus.tsx +++ b/frontend/src/pages/projects/notebook/NotebookStateStatus.tsx @@ -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'; @@ -52,9 +52,9 @@ const NotebookStateStatus: React.FC = ({ }; } if (isRunning) { - return { label: 'Running', color: 'green', icon: }; + return { label: 'Running', color: 'green', icon: }; } - return { label: 'Stopped', color: 'grey', icon: }; + return { label: 'Stopped', color: 'grey', icon: }; }, [isError, isRunning, isStarting, isStopping]); const StatusLabel = ( diff --git a/frontend/src/pages/projects/notebook/utils.ts b/frontend/src/pages/projects/notebook/utils.ts index 8114680860..a23dd93645 100644 --- a/frontend/src/pages/projects/notebook/utils.ts +++ b/frontend/src/pages/projects/notebook/utils.ts @@ -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 => @@ -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'], + }), + }); +}; diff --git a/frontend/src/pages/projects/screens/detail/notebooks/NotebookList.tsx b/frontend/src/pages/projects/screens/detail/notebooks/NotebookList.tsx index 09a488652c..d363a1c8aa 100644 --- a/frontend/src/pages/projects/screens/detail/notebooks/NotebookList.tsx +++ b/frontend/src/pages/projects/screens/detail/notebooks/NotebookList.tsx @@ -5,30 +5,35 @@ import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import { ProjectSectionID } from '~/pages/projects/screens/detail/types'; import { ProjectSectionTitles } from '~/pages/projects/screens/detail/const'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; -import { FAST_POLL_INTERVAL } from '~/utilities/const'; +import { FAST_POLL_INTERVAL, POLL_INTERVAL } from '~/utilities/const'; import DetailsSection from '~/pages/projects/screens/detail/DetailsSection'; import EmptyDetailsView from '~/components/EmptyDetailsView'; import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton'; import { ProjectObjectType, typedEmptyImage } from '~/concepts/design/utils'; +import useRefreshInterval from '~/utilities/useRefreshInterval'; import NotebookTable from './NotebookTable'; const NotebookList: React.FC = () => { const { currentProject, - notebooks: { data: notebookStates, loaded, error: loadError, refresh: refreshNotebooks }, + notebooks: { data: notebookStates, loaded, error: loadError }, refreshAllProjectData: refresh, } = React.useContext(ProjectDetailsContext); const navigate = useNavigate(); const projectName = currentProject.metadata.name; const isNotebooksEmpty = notebookStates.length === 0; - React.useEffect(() => { - let interval: ReturnType; - if (notebookStates.some((notebookState) => notebookState.isStarting)) { - interval = setInterval(() => refreshNotebooks(), FAST_POLL_INTERVAL); - } - return () => clearInterval(interval); - }, [notebookStates, refreshNotebooks]); + useRefreshInterval(FAST_POLL_INTERVAL, () => + notebookStates + .filter((notebookState) => notebookState.isStarting || notebookState.isStopping) + .forEach((notebookState) => notebookState.refresh()), + ); + + useRefreshInterval(POLL_INTERVAL, () => + notebookStates + .filter((notebookState) => !notebookState.isStarting && !notebookState.isStopping) + .forEach((notebookState) => notebookState.refresh()), + ); return ( = ({ notebookSize Limits - {limits?.cpu ?? 'Unknown'} CPU, {limits?.memory ?? 'Unknown'} Memory + {limits?.cpu ?? 'Unknown'} CPU, {formatMemory(limits?.memory) || 'Unknown'} Memory Requests - {requests?.cpu ?? 'Unknown'} CPU, {requests?.memory ?? 'Unknown'} Memory + {requests?.cpu ?? 'Unknown'} CPU, {formatMemory(requests?.memory) || 'Unknown'} Memory diff --git a/frontend/src/pages/projects/screens/detail/notebooks/NotebookTableRow.tsx b/frontend/src/pages/projects/screens/detail/notebooks/NotebookTableRow.tsx index ae558c73e7..e8a0b830e4 100644 --- a/frontend/src/pages/projects/screens/detail/notebooks/NotebookTableRow.tsx +++ b/frontend/src/pages/projects/screens/detail/notebooks/NotebookTableRow.tsx @@ -14,13 +14,22 @@ import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconBut import { getDescriptionFromK8sResource, getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; import { NotebookSize } from '~/types'; import NotebookStateStatus from '~/pages/projects/notebook/NotebookStateStatus'; -import { useNotebookActionsColumn } from '~/pages/projects/notebook/NotebookActionsColumn'; -import useNotebookDeploymentSize from './useNotebookDeploymentSize'; -import useNotebookImage from './useNotebookImage'; -import NotebookSizeDetails from './NotebookSizeDetails'; -import NotebookStorageBars from './NotebookStorageBars'; -import { NotebookImageDisplayName } from './NotebookImageDisplayName'; +import { NotebookActionsColumn } from '~/pages/projects/notebook/NotebookActionsColumn'; +import { computeNotebooksTolerations } from '~/utilities/tolerations'; +import { startNotebook, stopNotebook } from '~/api'; +import { currentlyHasPipelines } from '~/concepts/pipelines/elyra/utils'; +import { fireNotebookTrackingEvent } from '~/pages/projects/notebook/utils'; +import useNotebookAcceleratorProfile from '~/pages/projects/screens/detail/notebooks/useNotebookAcceleratorProfile'; +import useStopNotebookModalAvailability from '~/pages/projects/notebook/useStopNotebookModalAvailability'; +import { useAppContext } from '~/app/AppContext'; +import NotebookStateAction from '~/pages/projects/notebook/NotebookStateAction'; +import StopNotebookConfirmModal from '~/pages/projects/notebook/StopNotebookConfirmModal'; import { NotebookImageAvailability } from './const'; +import { NotebookImageDisplayName } from './NotebookImageDisplayName'; +import NotebookStorageBars from './NotebookStorageBars'; +import NotebookSizeDetails from './NotebookSizeDetails'; +import useNotebookImage from './useNotebookImage'; +import useNotebookDeploymentSize from './useNotebookDeploymentSize'; type NotebookTableRowProps = { obj: NotebookState; @@ -51,12 +60,41 @@ const NotebookTableRow: React.FC = ({ }, }; const [notebookImage, loaded, loadError] = useNotebookImage(obj.notebook); - const [ActionColumn, stopNotebook] = useNotebookActionsColumn( - currentProject, - obj, - canEnablePipelines, - onNotebookDelete, - ); + const acceleratorProfile = useNotebookAcceleratorProfile(obj.notebook); + const [dontShowModalValue] = useStopNotebookModalAvailability(); + const { dashboardConfig } = useAppContext(); + const [isOpenConfirm, setOpenConfirm] = React.useState(false); + const [inProgress, setInProgress] = React.useState(false); + const { name: notebookName, namespace: notebookNamespace } = obj.notebook.metadata; + + const onStart = React.useCallback(() => { + setInProgress(true); + const tolerationSettings = computeNotebooksTolerations(dashboardConfig, obj.notebook); + startNotebook( + obj.notebook, + tolerationSettings, + canEnablePipelines && !currentlyHasPipelines(obj.notebook), + ).then(() => { + fireNotebookTrackingEvent('started', obj.notebook, notebookSize, acceleratorProfile); + obj.refresh().then(() => setInProgress(false)); + }); + }, [dashboardConfig, obj, canEnablePipelines, notebookSize, acceleratorProfile]); + + const handleStop = React.useCallback(() => { + fireNotebookTrackingEvent('stopped', obj.notebook, notebookSize, acceleratorProfile); + setInProgress(true); + stopNotebook(notebookName, notebookNamespace).then(() => { + obj.refresh().then(() => setInProgress(false)); + }); + }, [acceleratorProfile, notebookName, notebookNamespace, notebookSize, obj]); + + const onStop = React.useCallback(() => { + if (dontShowModalValue) { + handleStop(); + } else { + setOpenConfirm(true); + } + }, [dontShowModalValue, handleStop]); return ( @@ -153,12 +191,28 @@ const NotebookTableRow: React.FC = ({ ) : null} - + + + + - {!compact ? {ActionColumn} : null} + {!compact ? ( + + + + ) : null} {!compact ? ( @@ -188,6 +242,17 @@ const NotebookTableRow: React.FC = ({ ) : null} + {isOpenConfirm ? ( + { + if (confirmStatus) { + handleStop(); + } + setOpenConfirm(false); + }} + /> + ) : null} ); }; diff --git a/frontend/src/pages/projects/screens/detail/notebooks/data.ts b/frontend/src/pages/projects/screens/detail/notebooks/data.ts index c63da08c15..68e5011f3e 100644 --- a/frontend/src/pages/projects/screens/detail/notebooks/data.ts +++ b/frontend/src/pages/projects/screens/detail/notebooks/data.ts @@ -12,7 +12,7 @@ export const columns: SortableData[] = [ { field: 'name', label: 'Name', - width: 30, + width: 25, sortable: (a, b) => getDisplayNameFromK8sResource(a.notebook).localeCompare( getDisplayNameFromK8sResource(b.notebook), @@ -34,6 +34,12 @@ export const columns: SortableData[] = [ field: 'status', label: 'Status', sortable: (a, b) => getNotebookStatusPriority(a) - getNotebookStatusPriority(b), + modifier: 'fitContent', + }, + { + field: '', + label: '', + sortable: false, }, { field: 'open', diff --git a/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx b/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx index b364778a0b..d0eef3bd59 100644 --- a/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx +++ b/frontend/src/pages/projects/screens/projects/ProjectTableRow.tsx @@ -1,15 +1,17 @@ import * as React from 'react'; import { Spinner, Text, TextVariants, Timestamp } from '@patternfly/react-core'; import { ActionsColumn, Tbody, Td, Tr } from '@patternfly/react-table'; +import { OffIcon, PlayIcon } from '@patternfly/react-icons'; import { ProjectKind } from '~/k8sTypes'; -import NotebookIcon from '~/images/icons/NotebookIcon'; import useProjectTableRowItems from '~/pages/projects/screens/projects/useProjectTableRowItems'; import { getProjectOwner } from '~/concepts/projects/utils'; import ProjectTableRowNotebookTable from '~/pages/projects/screens/projects/ProjectTableRowNotebookTable'; import { TableRowTitleDescription } from '~/components/table'; import ResourceNameTooltip from '~/components/ResourceNameTooltip'; import { getDescriptionFromK8sResource } from '~/concepts/k8s/utils'; -import { useWatchNotebooks } from '~/utilities/useWatchNotebooks'; +import useProjectNotebookStates from '~/pages/projects/notebook/useProjectNotebookStates'; +import { FAST_POLL_INTERVAL, POLL_INTERVAL } from '~/utilities/const'; +import useRefreshInterval from '~/utilities/useRefreshInterval'; import ProjectLink from './ProjectLink'; // Plans to add other expandable columns in the future @@ -37,12 +39,30 @@ const ProjectTableRow: React.FC = ({ setEditData, setDeleteData, ); - const [notebooks, loaded] = useWatchNotebooks(project.metadata.name); + const [notebookStates, loaded, , refresh] = useProjectNotebookStates(project.metadata.name); + const runningCount = notebookStates.filter( + (notebookState) => notebookState.isRunning || notebookState.isStarting, + ).length; + const stoppedCount = notebookStates.filter( + (notebookState) => notebookState.isStopping || notebookState.isStopped, + ).length; const toggleExpandColumn = (colIndex: ExpandableColumns) => { setExpandColumn(expandColumn === colIndex ? undefined : colIndex); }; + useRefreshInterval(FAST_POLL_INTERVAL, () => + notebookStates + .filter((notebookState) => notebookState.isStarting || notebookState.isStopping) + .forEach((notebookState) => notebookState.refresh()), + ); + + useRefreshInterval(POLL_INTERVAL, () => + notebookStates + .filter((notebookState) => !notebookState.isStarting && !notebookState.isStopping) + .forEach((notebookState) => notebookState.refresh()), + ); + return ( @@ -80,7 +100,7 @@ const ProjectTableRow: React.FC = ({ = ({ } data-testid="notebook-column-expand" > - - - {loaded ? notebooks.length : } - + {!loaded ? ( + + ) : ( +
+ + {runningCount} + + {stoppedCount} +
+ )} = ({ borderTopColor: 'var(--pf-v5-global--BorderColor--100)', }} > - + ) : null} diff --git a/frontend/src/pages/projects/screens/projects/ProjectTableRowNotebookTable.tsx b/frontend/src/pages/projects/screens/projects/ProjectTableRowNotebookTable.tsx index 64851e559f..139e11b54c 100644 --- a/frontend/src/pages/projects/screens/projects/ProjectTableRowNotebookTable.tsx +++ b/frontend/src/pages/projects/screens/projects/ProjectTableRowNotebookTable.tsx @@ -1,21 +1,23 @@ import * as React from 'react'; import { Table } from '~/components/table'; import { NotebookKind, ProjectKind } from '~/k8sTypes'; -import { useNotebooksStates } from '~/pages/projects/notebook/useNotebooksStates'; import CanEnableElyraPipelinesCheck from '~/concepts/pipelines/elyra/CanEnableElyraPipelinesCheck'; import ProjectTableRowNotebookTableRow from '~/pages/projects/screens/projects/ProjectTableRowNotebookTableRow'; import DeleteNotebookModal from '~/pages/projects/notebook/DeleteNotebookModal'; +import { NotebookState } from '~/pages/projects/notebook/types'; +import { FetchStateRefreshPromise } from '~/utilities/useFetchState'; import { columns } from './notebookTableData'; type ProjectTableRowNotebookTableProps = { - notebooks: NotebookKind[]; + notebookStates: NotebookState[]; obj: ProjectKind; + refresh: FetchStateRefreshPromise; }; const ProjectTableRowNotebookTable: React.FC = ({ - notebooks, + notebookStates, obj: project, + refresh, }) => { - const [notebookStates, loaded, , refresh] = useNotebooksStates(notebooks, project.metadata.name); const [notebookToDelete, setNotebookToDelete] = React.useState(); return ( @@ -24,9 +26,6 @@ const ProjectTableRowNotebookTable: React.FC <> { - const [ActionColumn, stopNotebook] = useNotebookActionsColumn( - project, - notebookState, - enablePipelines, - onNotebookDelete, - ); + const { notebook, refresh } = notebookState; + const acceleratorProfile = useNotebookAcceleratorProfile(notebook); + const [dontShowModalValue] = useStopNotebookModalAvailability(); + const { dashboardConfig } = useAppContext(); + const { size } = useNotebookDeploymentSize(notebook); + const [isOpenConfirm, setOpenConfirm] = React.useState(false); + const [inProgress, setInProgress] = React.useState(false); + const { name: notebookName, namespace: notebookNamespace } = notebook.metadata; + + const onStart = React.useCallback(() => { + setInProgress(true); + const tolerationSettings = computeNotebooksTolerations(dashboardConfig, notebook); + startNotebook( + notebook, + tolerationSettings, + enablePipelines && !currentlyHasPipelines(notebook), + ).then(() => { + fireNotebookTrackingEvent('started', notebook, size, acceleratorProfile); + refresh().then(() => setInProgress(false)); + }); + }, [acceleratorProfile, dashboardConfig, enablePipelines, notebook, refresh, size]); + + const handleStop = React.useCallback(() => { + fireNotebookTrackingEvent('stopped', notebook, size, acceleratorProfile); + setInProgress(true); + stopNotebook(notebookName, notebookNamespace).then(() => { + refresh().then(() => setInProgress(false)); + }); + }, [acceleratorProfile, notebook, notebookName, notebookNamespace, refresh, size]); + + const onStop = React.useCallback(() => { + if (dontShowModalValue) { + handleStop(); + } else { + setOpenConfirm(true); + } + }, [dontShowModalValue, handleStop]); + return ( - + + + {isOpenConfirm ? ( + { + if (confirmStatus) { + handleStop(); + } + setOpenConfirm(false); + }} + /> + ) : null} ); }; diff --git a/frontend/src/pages/projects/screens/projects/notebookTableData.tsx b/frontend/src/pages/projects/screens/projects/notebookTableData.tsx index 8fe71a3510..19d3792ea2 100644 --- a/frontend/src/pages/projects/screens/projects/notebookTableData.tsx +++ b/frontend/src/pages/projects/screens/projects/notebookTableData.tsx @@ -32,7 +32,13 @@ export const columns: SortableData[] = [ field: 'status', label: 'Status', sortable: (a, b) => getNotebookStatusValue(a) - getNotebookStatusValue(b), - width: 40, + modifier: 'fitContent', + }, + { + field: '', + label: '', + sortable: false, + width: 20, }, { field: 'kebab', diff --git a/frontend/src/pages/projects/screens/projects/tableData.tsx b/frontend/src/pages/projects/screens/projects/tableData.tsx index 469343243f..9a01e67a97 100644 --- a/frontend/src/pages/projects/screens/projects/tableData.tsx +++ b/frontend/src/pages/projects/screens/projects/tableData.tsx @@ -1,8 +1,23 @@ +import { OffIcon, PlayIcon } from '@patternfly/react-icons'; +import * as React from 'react'; import { SortableData } from '~/components/table'; import { ProjectKind } from '~/k8sTypes'; import { getProjectCreationTime } from '~/concepts/projects/utils'; import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; +const WorkBenchDescription = ( +
+
+ + Indicates number of running or starting workbenches. +
+
+ + Indicates number of stopped workbenches. +
+
+); + export const columns: SortableData[] = [ { field: 'name', @@ -22,6 +37,10 @@ export const columns: SortableData[] = [ label: 'Workbenches', sortable: false, width: 30, + info: { + popoverProps: { headerContent: 'Workbench counts', hasAutoWidth: true }, + popover: WorkBenchDescription, + }, }, { field: 'kebab', diff --git a/frontend/src/pages/projects/utils.ts b/frontend/src/pages/projects/utils.ts index a045afdd67..b6b175adf5 100644 --- a/frontend/src/pages/projects/utils.ts +++ b/frontend/src/pages/projects/utils.ts @@ -1,12 +1,13 @@ import { NotebookKind, PersistentVolumeClaimKind } from '~/k8sTypes'; import { NotebookSize } from '~/types'; +import { formatMemory } from '~/utilities/valueUnits'; import { NotebookState } from './notebook/types'; export const getNotebookStatusPriority = (notebookState: NotebookState): number => notebookState.isRunning ? 1 : notebookState.isStarting ? 2 : 3; export const getPvcTotalSize = (pvc: PersistentVolumeClaimKind): string => - pvc.status?.capacity?.storage || pvc.spec.resources.requests.storage; + formatMemory(pvc.status?.capacity?.storage || pvc.spec.resources.requests.storage); export const getCustomNotebookSize = ( existingNotebook: NotebookKind | undefined, diff --git a/frontend/src/utilities/__tests__/valueUnits.spec.ts b/frontend/src/utilities/__tests__/valueUnits.spec.ts index be5f80e4bb..e27918d5e0 100644 --- a/frontend/src/utilities/__tests__/valueUnits.spec.ts +++ b/frontend/src/utilities/__tests__/valueUnits.spec.ts @@ -8,6 +8,7 @@ import { isLarger, convertToUnit, MEMORY_UNITS_FOR_PARSING, + formatMemory, } from '~/utilities/valueUnits'; describe('splitValueUnit', () => { @@ -189,3 +190,27 @@ describe('isMemoryLimitLarger', () => { expect(isMemoryLimitLarger(undefined, undefined)).toBe(false); }); }); + +describe('formatMemory', () => { + it('should return undefined if no value is provided', () => { + expect(formatMemory(undefined)).toBeUndefined(); + }); + + it('should return the original value if it cannot be parsed', () => { + expect(formatMemory('invalidValue')).toEqual('invalidValue'); + }); + + it('should return the formatted value for valid inputs', () => { + expect(formatMemory('1Mi')).toEqual('1MiB'); + expect(formatMemory('2Gi')).toEqual('2GiB'); + expect(formatMemory('3Mi')).toEqual('3MiB'); + expect(formatMemory('4Gi')).toEqual('4GiB'); + expect(formatMemory('1.5Mi')).toEqual('1.5MiB'); + expect(formatMemory('2.5Gi')).toEqual('2.5GiB'); + }); + + it('should handle cases with no unit', () => { + expect(formatMemory('1')).toEqual('1'); + expect(formatMemory('1.5')).toEqual('1.5'); + }); +}); diff --git a/frontend/src/utilities/useRefreshInterval.ts b/frontend/src/utilities/useRefreshInterval.ts index 4cc029ac81..634d8b0ff0 100644 --- a/frontend/src/utilities/useRefreshInterval.ts +++ b/frontend/src/utilities/useRefreshInterval.ts @@ -6,7 +6,7 @@ const useRefreshInterval = (refreshInterval: number, callback: () => void): void cb.current = callback; React.useEffect(() => { - const timer = setInterval(cb.current, refreshInterval); + const timer = setInterval(() => cb.current(), refreshInterval); return () => clearInterval(timer); }, [refreshInterval]); }; diff --git a/frontend/src/utilities/valueUnits.ts b/frontend/src/utilities/valueUnits.ts index 32c88ebbf1..9b820a3a82 100644 --- a/frontend/src/utilities/valueUnits.ts +++ b/frontend/src/utilities/valueUnits.ts @@ -22,8 +22,8 @@ export const CPU_UNITS: UnitOption[] = [ { name: 'Milicores', unit: 'm', weight: 1 }, ]; export const MEMORY_UNITS_FOR_SELECTION: UnitOption[] = [ - { name: 'Gi', unit: 'Gi', weight: 1024 }, - { name: 'Mi', unit: 'Mi', weight: 1 }, + { name: 'GiB', unit: 'Gi', weight: 1024 }, + { name: 'MiB', unit: 'Mi', weight: 1 }, ]; export const MEMORY_UNITS_FOR_PARSING: UnitOption[] = [ { name: 'EB', unit: 'E', weight: 1000 ** 6 }, @@ -155,3 +155,19 @@ export const isMemoryLimitLarger = ( return isLarger(limitMemory, requestMemory, MEMORY_UNITS_FOR_PARSING, isEqualOkay); }; + +export const formatMemory = ( + value: T, +): T | ValueUnitString => { + if (!value) { + return value; + } + + const match = value.match(/^(\d*\.?\d*)(.*)$/); + if (!(match && match[1] && match[2])) { + return value; + } + return `${match[1]}${ + MEMORY_UNITS_FOR_PARSING.find((o) => o.unit === match[2])?.name || match[2] + }`; +}; diff --git a/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml b/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml index 512d2a7dd0..b35fb315b7 100644 --- a/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml +++ b/manifests/common/crd/odhdashboardconfigs.opendatahub.io.crd.yaml @@ -53,7 +53,7 @@ spec: type: boolean disablePipelines: type: boolean - disableBiasMetrics: + disableTrustyBiasMetrics: type: boolean disablePerformanceMetrics: type: boolean diff --git a/manifests/rhoai/shared/odhdashboardconfig/odhdashboardconfig.yaml b/manifests/rhoai/shared/odhdashboardconfig/odhdashboardconfig.yaml index 131131dae0..b434ff229a 100644 --- a/manifests/rhoai/shared/odhdashboardconfig/odhdashboardconfig.yaml +++ b/manifests/rhoai/shared/odhdashboardconfig/odhdashboardconfig.yaml @@ -19,7 +19,7 @@ spec: disableModelServing: false disableProjectSharing: false disableCustomServingRuntimes: false - disableBiasMetrics: false + disableTrustyBiasMetrics: false disablePerformanceMetrics: false disableAcceleratorProfiles: false disableKServe: false
@@ -35,9 +77,34 @@ const ProjectTableRowNotebookTableRow: React.FC - + {ActionColumn} + + + +