diff --git a/frontend/src/__mocks__/mockNotebookK8sResource.ts b/frontend/src/__mocks__/mockNotebookK8sResource.ts index 02a0499924..ef8e631075 100644 --- a/frontend/src/__mocks__/mockNotebookK8sResource.ts +++ b/frontend/src/__mocks__/mockNotebookK8sResource.ts @@ -146,6 +146,10 @@ export const mockNotebookK8sResource = ({ mountPath: '/opt/app-root/src', name, }, + { + mountPath: '/opt/app-root/src/root', + name: 'test-storage-1', + }, ...additionalVolumeMounts, ], workingDir: '/opt/app-root/src', @@ -233,6 +237,12 @@ export const mockNotebookK8sResource = ({ claimName: name, }, }, + { + name: 'test-storage-1', + persistentVolumeClaim: { + claimName: 'test-storage-1', + }, + }, { name: 'oauth-config', secret: { diff --git a/frontend/src/__tests__/cypress/cypress/pages/workbench.ts b/frontend/src/__tests__/cypress/cypress/pages/workbench.ts index dee82e7e10..e369f0fdd6 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/workbench.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/workbench.ts @@ -129,8 +129,24 @@ class NotebookRow extends TableRow { return this; } - toggleExpandableContent() { - this.find().findByRole('button', { name: 'Details' }).click(); + shouldHaveClusterStorageTitle() { + this.findExpansion() + .findByTestId('notebook-storage-bar-title') + .should('have.text', 'Cluster storage'); + return this; + } + + shouldHaveMountPath(name: string) { + this.findExpansion().findByTestId('storage-mount-path').contains(name); + return this; + } + + findExpansionButton() { + return this.find().findByTestId('notebook-table-expand-cell'); + } + + findExpansion() { + return this.find().siblings(); } findAddStorageButton() { 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 c14950243c..fef7f93dff 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 @@ -17,7 +17,6 @@ import { editSpawnerPage, notFoundSpawnerPage, notebookConfirmModal, - storageModal, workbenchPage, } from '~/__tests__/cypress/cypress/pages/workbench'; import { verifyRelativeURL } from '~/__tests__/cypress/cypress/utils/url'; @@ -165,7 +164,10 @@ const initIntercepts = ({ ), ); cy.interceptK8sList(SecretModel, mockK8sResourceList([mockSecretK8sResource({})])); - cy.interceptK8sList(PVCModel, mockK8sResourceList([mockPVCK8sResource({})])); + cy.interceptK8sList( + PVCModel, + mockK8sResourceList([mockPVCK8sResource({ name: 'test-storage-1' })]), + ); cy.interceptK8s('POST', ConfigMapModel, mockConfigMap({})).as('createConfigMap'); @@ -327,7 +329,7 @@ describe('Workbench page', () => { template: { spec: { volumes: [ - { name: 'test-storage', persistentVolumeClaim: { claimName: 'test-storage' } }, + { name: 'test-storage-1', persistentVolumeClaim: { claimName: 'test-storage-1' } }, ], }, }, @@ -454,13 +456,6 @@ describe('Workbench page', () => { workbenchPage.findNotebookTableHeaderButton('Status').should(be.sortAscending); workbenchPage.findNotebookTableHeaderButton('Status').click(); workbenchPage.findNotebookTableHeaderButton('Status').should(be.sortDescending); - - //expandable table - notebookRow.toggleExpandableContent(); - notebookRow.findAddStorageButton().click(); - storageModal.selectExistingPersistentStorage('Test Storage'); - storageModal.findMountField().fill('data'); - storageModal.findSubmitButton().click(); }); it('Validate the notebook status when workbench is stopped and starting', () => { @@ -650,6 +645,16 @@ describe('Workbench page', () => { verifyRelativeURL('/projects/test-project?section=overview'); }); + it('Expanded workbench table row', () => { + initIntercepts({}); + workbenchPage.visit('test-project'); + const notebookRow = workbenchPage.getNotebookRow('Test Notebook'); + notebookRow.findExpansionButton().click(); + notebookRow.findExpansion().should('be.visible'); + notebookRow.shouldHaveClusterStorageTitle(); + notebookRow.shouldHaveMountPath('/opt/app-root/src/root'); + }); + it('Delete Workbench', () => { initIntercepts({}); workbenchPage.visit('test-project'); diff --git a/frontend/src/api/k8s/__tests__/notebooks.spec.ts b/frontend/src/api/k8s/__tests__/notebooks.spec.ts index e641102339..bbdc9476e1 100644 --- a/frontend/src/api/k8s/__tests__/notebooks.spec.ts +++ b/frontend/src/api/k8s/__tests__/notebooks.spec.ts @@ -780,6 +780,12 @@ describe('removeNotebookPVC', () => { path: '/spec/template/spec/volumes', value: [ { name: notebookName, persistentVolumeClaim: { claimName: notebookName } }, + { + name: 'test-storage-1', + persistentVolumeClaim: { + claimName: 'test-storage-1', + }, + }, { name: 'oauth-config', secret: { secretName: 'workbench-oauth-config' } }, { name: 'tls-certificates', secret: { secretName: 'workbench-tls' } }, ], @@ -787,7 +793,13 @@ describe('removeNotebookPVC', () => { { op: 'replace', path: '/spec/template/spec/containers/0/volumeMounts', - value: [{ mountPath: '/opt/app-root/src', name: notebookName }], + value: [ + { mountPath: '/opt/app-root/src', name: notebookName }, + { + mountPath: '/opt/app-root/src/root', + name: 'test-storage-1', + }, + ], }, ], queryOptions: { name: notebookName, ns: namespace, queryParams: {} }, @@ -853,6 +865,12 @@ describe('removeNotebookPVC', () => { path: '/spec/template/spec/volumes', value: [ { name: notebookName, persistentVolumeClaim: { claimName: notebookName } }, + { + name: 'test-storage-1', + persistentVolumeClaim: { + claimName: 'test-storage-1', + }, + }, { name: 'oauth-config', secret: { secretName: 'workbench-oauth-config' } }, { name: 'tls-certificates', secret: { secretName: 'workbench-tls' } }, ], @@ -860,7 +878,13 @@ describe('removeNotebookPVC', () => { { op: 'replace', path: '/spec/template/spec/containers/0/volumeMounts', - value: [{ mountPath: '/opt/app-root/src', name: notebookName }], + value: [ + { mountPath: '/opt/app-root/src', name: notebookName }, + { + mountPath: '/opt/app-root/src/root', + name: 'test-storage-1', + }, + ], }, ], queryOptions: { name: notebookName, ns: namespace, queryParams: {} }, diff --git a/frontend/src/components/InlineTruncatedClipboardCopy.tsx b/frontend/src/components/InlineTruncatedClipboardCopy.tsx index a00bd9dac7..4b19fb448a 100644 --- a/frontend/src/components/InlineTruncatedClipboardCopy.tsx +++ b/frontend/src/components/InlineTruncatedClipboardCopy.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; type Props = { textToCopy: string; testId?: string; + maxWidth?: number; }; /** Hopefully PF will add some flexibility with ClipboardCopy @@ -11,10 +12,10 @@ type Props = { * https://github.com/patternfly/patternfly-react/issues/10890 **/ -const InlineTruncatedClipboardCopy: React.FC = ({ textToCopy, testId }) => ( +const InlineTruncatedClipboardCopy: React.FC = ({ textToCopy, testId, maxWidth }) => ( { diff --git a/frontend/src/concepts/design/HeaderIcon.tsx b/frontend/src/concepts/design/HeaderIcon.tsx index 4d42d94e56..9cee2b93e1 100644 --- a/frontend/src/concepts/design/HeaderIcon.tsx +++ b/frontend/src/concepts/design/HeaderIcon.tsx @@ -10,6 +10,7 @@ import { interface HeaderIconProps { size?: number; padding?: number; + display?: string; image?: string; type: ProjectObjectType; sectionType?: SectionType; @@ -18,13 +19,14 @@ interface HeaderIconProps { const HeaderIcon: React.FC = ({ size = 40, padding = 2, + display = 'inline-block', image, type, sectionType, }) => (
void; -}; - -const AddNotebookStorage: React.FC = ({ notebook, onClose }) => { - const [existingData, setExistingData, resetDefaults] = useExistingStorageDataObjectForNotebook(); - const [isSubmitting, setIsSubmitting] = React.useState(false); - const [error, setError] = React.useState(); - const notebookDisplayName = getDisplayNameFromK8sResource(notebook); - const inUseMountPaths = getNotebookMountPaths(notebook); - const restartNotebooks = useWillNotebooksRestart([notebook.metadata.name]); - - const canSubmit = - !isSubmitting && - !!existingData.name && - !!existingData.mountPath.value && - !existingData.mountPath.error; - - const beforeClose = (submitted: boolean) => { - resetDefaults(); - onClose(submitted); - setIsSubmitting(false); - }; - - const submit = () => { - setIsSubmitting(true); - attachNotebookPVC( - notebook.metadata.name, - notebook.metadata.namespace, - existingData.name, - existingData.mountPath.value, - ) - .then(() => { - beforeClose(true); - }) - .catch((e) => { - setError(e); - setIsSubmitting(false); - }); - }; - - return ( - beforeClose(false)} - actions={[ - , - , - ]} - > - - -
{ - e.preventDefault(); - submit(); - }} - > - setExistingData('name', storage)} - /> - setExistingData('mountPath', mountPath)} - /> - -
- {restartNotebooks.length !== 0 && ( - - - - )} - {error && ( - - - {error.message} - - - )} -
-
- ); -}; - -export default AddNotebookStorage; diff --git a/frontend/src/pages/projects/screens/detail/notebooks/NotebookStorageBars.tsx b/frontend/src/pages/projects/screens/detail/notebooks/NotebookStorageBars.tsx index a48419d06b..9e08ab6a2d 100644 --- a/frontend/src/pages/projects/screens/detail/notebooks/NotebookStorageBars.tsx +++ b/frontend/src/pages/projects/screens/detail/notebooks/NotebookStorageBars.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { Alert, - Button, + Flex, + FlexItem, List, ListItem, Spinner, @@ -9,26 +10,32 @@ import { StackItem, Text, } from '@patternfly/react-core'; -import { PlusCircleIcon } from '@patternfly/react-icons'; + import { NotebookKind } from '~/k8sTypes'; import useNotebookPVCItems from '~/pages/projects/pvc/useNotebookPVCItems'; import StorageSizeBar from '~/pages/projects/components/StorageSizeBars'; import { getNotebookPVCMountPathMap } from '~/pages/projects/notebook/utils'; import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; +import { ProjectObjectType } from '~/concepts/design/utils'; +import HeaderIcon from '~/concepts/design/HeaderIcon'; +import InlineTruncatedClipboardCopy from '~/components/InlineTruncatedClipboardCopy'; +import ShowAllButton from './ShowAllButton'; type NotebookStorageBarsProps = { notebook: NotebookKind; - onAddStorage: (notebook: NotebookKind) => void; }; -const NotebookStorageBars: React.FC = ({ notebook, onAddStorage }) => { +const NotebookStorageBars: React.FC = ({ notebook }) => { const [pvcs, loaded, loadError] = useNotebookPVCItems(notebook); const mountFolderMap = getNotebookPVCMountPathMap(notebook); + const [showAll, setShowAll] = React.useState(false); + const defaultVisibleLength = 2; + const visiblePvcs = showAll ? pvcs : pvcs.slice(0, defaultVisibleLength); return ( - + - Workbench storages + Cluster storage {!loaded ? ( @@ -39,40 +46,52 @@ const NotebookStorageBars: React.FC = ({ notebook, onA ) : ( - {pvcs.map((pvc) => ( + {visiblePvcs.map((pvc) => ( - - - {getDisplayNameFromK8sResource(pvc)} - - - - - - - Mount path:{' '} - {mountFolderMap[pvc.metadata.name] - ? mountFolderMap[pvc.metadata.name] - : 'Unknown'} - - - + + + + + + {getDisplayNameFromK8sResource(pvc)} + + + + + + + Mount path:{' '} + {mountFolderMap[pvc.metadata.name] ? ( + + ) : ( + 'unknown' + )} + + + + + ))} - - - )} + + setShowAll(!showAll)} + totalSize={pvcs.length} + /> + ); }; diff --git a/frontend/src/pages/projects/screens/detail/notebooks/NotebookTable.tsx b/frontend/src/pages/projects/screens/detail/notebooks/NotebookTable.tsx index 3bc5decd87..d3ed5ce41b 100644 --- a/frontend/src/pages/projects/screens/detail/notebooks/NotebookTable.tsx +++ b/frontend/src/pages/projects/screens/detail/notebooks/NotebookTable.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Table } from '~/components/table'; import { NotebookKind } from '~/k8sTypes'; import DeleteNotebookModal from '~/pages/projects/notebook/DeleteNotebookModal'; -import AddNotebookStorage from '~/pages/projects/pvc/AddNotebookStorage'; import { NotebookState } from '~/pages/projects/notebook/types'; import CanEnableElyraPipelinesCheck from '~/concepts/pipelines/elyra/CanEnableElyraPipelinesCheck'; import { ProjectDetailsContext } from '~/pages/projects/ProjectDetailsContext'; @@ -17,7 +16,6 @@ type NotebookTableProps = { const NotebookTable: React.FC = ({ notebookStates, refresh }) => { const { currentProject } = React.useContext(ProjectDetailsContext); - const [addNotebookStorage, setAddNotebookStorage] = React.useState(); const [notebookToDelete, setNotebookToDelete] = React.useState(); return ( @@ -38,7 +36,6 @@ const NotebookTable: React.FC = ({ notebookStates, refresh } rowIndex={i} obj={notebookState} onNotebookDelete={setNotebookToDelete} - onNotebookAddStorage={setAddNotebookStorage} canEnablePipelines={canEnablePipelines} showOutOfDateElyraInfo={showImpactedNotebookInfo(notebookState.notebook)} /> @@ -48,17 +45,6 @@ const NotebookTable: React.FC = ({ notebookStates, refresh } )} - {addNotebookStorage ? ( - { - if (submitted) { - refresh(); - } - setAddNotebookStorage(undefined); - }} - /> - ) : null} {notebookToDelete ? ( void; - onNotebookAddStorage: (notebook: NotebookKind) => void; canEnablePipelines: boolean; compact?: boolean; showOutOfDateElyraInfo: boolean; @@ -36,7 +35,6 @@ const NotebookTableRow: React.FC = ({ obj, rowIndex, onNotebookDelete, - onNotebookAddStorage, canEnablePipelines, compact, showOutOfDateElyraInfo, @@ -65,6 +63,7 @@ const NotebookTableRow: React.FC = ({ {!compact ? ( = ({ - + diff --git a/frontend/src/pages/projects/screens/detail/notebooks/ShowAllButton.tsx b/frontend/src/pages/projects/screens/detail/notebooks/ShowAllButton.tsx new file mode 100644 index 0000000000..7846116ac5 --- /dev/null +++ b/frontend/src/pages/projects/screens/detail/notebooks/ShowAllButton.tsx @@ -0,0 +1,35 @@ +import { Badge, Button, Flex, FlexItem } from '@patternfly/react-core'; +import React from 'react'; + +type ShowAllButtonProps = { + visibleLength: number; + isExpanded: boolean; + totalSize: number; + onToggle: () => void; +}; + +const ShowAllButton: React.FC = ({ + visibleLength, + totalSize, + onToggle, + isExpanded, +}) => { + if (visibleLength >= totalSize) { + return null; + } + + return ( + + + + + + {!isExpanded && {`${totalSize - visibleLength} more`}} + + + ); +}; + +export default ShowAllButton;