From d9d1b09308f3d939e649eab76858175c34409c7f Mon Sep 17 00:00:00 2001 From: Vojtech Szocs Date: Wed, 19 Nov 2025 21:29:46 +0000 Subject: [PATCH] Migrate storage related tables to ConsoleDataView --- .../src/components/nodes/NodesPage.tsx | 4 +- .../volume-snapshot/volume-snapshot-class.tsx | 200 ++++--- .../volume-snapshot-content.tsx | 274 +++++----- .../volume-snapshot/volume-snapshot.tsx | 496 +++++++++--------- .../components/persistent-volume-claim.tsx | 406 +++++++++----- .../public/components/persistent-volume.tsx | 277 ++++++---- frontend/public/components/storage-class.tsx | 273 ++++++---- 7 files changed, 1105 insertions(+), 825 deletions(-) diff --git a/frontend/packages/console-app/src/components/nodes/NodesPage.tsx b/frontend/packages/console-app/src/components/nodes/NodesPage.tsx index 34da510ad2..959dc99e95 100644 --- a/frontend/packages/console-app/src/components/nodes/NodesPage.tsx +++ b/frontend/packages/console-app/src/components/nodes/NodesPage.tsx @@ -648,11 +648,11 @@ const NodeList: React.FC = ({ type NodeRowItem = NodeKind | NodeCertificateSigningRequestKind; -interface NodeFilters extends ResourceFilters { +type NodeFilters = ResourceFilters & { status: string[]; roles: string[]; architecture: string[]; -} +}; const useWatchCSRs = (): [CertificateSigningRequestKind[], boolean, unknown] => { const [isAllowed, checkIsLoading] = useAccessReview({ diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-class.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-class.tsx index b880e82123..96fc10aecc 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-class.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-class.tsx @@ -1,21 +1,19 @@ import * as React from 'react'; -import { css } from '@patternfly/react-styles'; -import { sortable } from '@patternfly/react-table'; import { useTranslation } from 'react-i18next'; +import { + actionsCellProps, + cellIsStickyProps, + getNameCellProps, + ConsoleDataView, +} from '@console/app/src/components/data-view/ConsoleDataView'; +import { GetDataViewRows } from '@console/app/src/components/data-view/types'; import { ListPageBody, - useListPageFilter, ListPageCreate, - ListPageFilter, ListPageHeader, - VirtualizedTable, TableColumn, - RowProps, } from '@console/dynamic-plugin-sdk/src/lib-core'; -import { TableData } from '@console/internal/components/factory'; -import { useActiveColumns } from '@console/internal/components/factory/Table/active-columns-hook'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; -import { Kebab } from '@console/internal/components/utils/kebab'; import { ResourceLink } from '@console/internal/components/utils/resource-link'; import { VolumeSnapshotClassModel } from '@console/internal/models'; import { @@ -25,95 +23,131 @@ import { referenceFor, } from '@console/internal/module/k8s'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; +import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; +import { DASH } from '@console/shared/src/constants/ui'; import { getAnnotations } from '@console/shared/src/selectors/common'; -const tableColumnInfo = [ - { id: 'name' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-md'), id: 'driver' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-md'), id: 'deletionPolicy' }, - { className: Kebab.columnClass, id: '' }, -]; +const kind = referenceForModel(VolumeSnapshotClassModel); + +const tableColumnInfo = [{ id: 'name' }, { id: 'driver' }, { id: 'deletionPolicy' }, { id: '' }]; + +const defaultSnapshotClassAnnotation = 'snapshot.storage.kubernetes.io/is-default-class'; -const defaultSnapshotClassAnnotation: string = 'snapshot.storage.kubernetes.io/is-default-class'; export const isDefaultSnapshotClass = (volumeSnapshotClass: VolumeSnapshotClassKind) => getAnnotations(volumeSnapshotClass, { defaultSnapshotClassAnnotation: 'false' })[ defaultSnapshotClassAnnotation ] === 'true'; -const Row: React.FC> = ({ obj }) => { - const { name } = obj?.metadata || {}; - const { deletionPolicy, driver } = obj || {}; - const resourceKind = referenceFor(obj); - const context = { [resourceKind]: obj }; - return ( - <> - - - {isDefaultSnapshotClass(obj) && ( - - – Default - - )} - - - {driver} - {deletionPolicy} - - - - - ); +const getDataViewRows: GetDataViewRows = (data, columns) => { + return data.map(({ obj }) => { + const name = obj.metadata?.name || ''; + const { deletionPolicy, driver } = obj; + const context = { [referenceFor(obj)]: obj }; + + const rowCells = { + [tableColumnInfo[0].id]: { + cell: ( + + {isDefaultSnapshotClass(obj) && ( + + – Default + + )} + + ), + props: getNameCellProps(name), + }, + [tableColumnInfo[1].id]: { + cell: driver, + }, + [tableColumnInfo[2].id]: { + cell: deletionPolicy, + }, + [tableColumnInfo[3].id]: { + cell: , + props: { + ...actionsCellProps, + }, + }, + }; + + return columns.map(({ id }) => { + const cell = rowCells[id]?.cell || DASH; + const props = rowCells[id]?.props || undefined; + return { + id, + props, + cell, + }; + }); + }); }; -const VolumeSnapshotClassTable: React.FC = (props) => { +const useVolumeSnapshotClassColumns = (): TableColumn[] => { const { t } = useTranslation(); - const getTableColumns = (): TableColumn[] => [ - { - title: t('console-app~Name'), - sort: 'metadata.name', - transforms: [sortable], - id: tableColumnInfo[0].id, - }, - { - title: t('console-app~Driver'), - sort: 'driver', - transforms: [sortable], - props: { className: tableColumnInfo[1].className }, - id: tableColumnInfo[1].id, - }, - { - title: t('console-app~Deletion policy'), - sort: 'deletionPolicy', - transforms: [sortable], - props: { className: tableColumnInfo[2].className }, - id: tableColumnInfo[2].id, - }, - { - title: '', - props: { className: tableColumnInfo[3].className }, - id: tableColumnInfo[3].id, - }, - ]; - const [columns] = useActiveColumns({ columns: getTableColumns() }); + + const columns: TableColumn[] = React.useMemo( + () => [ + { + title: t('console-app~Name'), + sort: 'metadata.name', + id: tableColumnInfo[0].id, + props: { ...cellIsStickyProps, modifier: 'nowrap' }, + }, + { + title: t('console-app~Driver'), + sort: 'driver', + id: tableColumnInfo[1].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Deletion policy'), + sort: 'deletionPolicy', + id: tableColumnInfo[2].id, + props: { modifier: 'nowrap' }, + }, + { + title: '', + id: tableColumnInfo[3].id, + props: { ...cellIsStickyProps }, + }, + ], + [t], + ); + + return columns; +}; + +const VolumeSnapshotClassTable: React.FCC = ({ + data, + loaded, + ...props +}) => { + const columns = useVolumeSnapshotClassColumns(); return ( - - {...props} - aria-label={t('console-app~VolumeSnapshotClasses')} - label={t('console-app~VolumeSnapshotClasses')} - columns={columns} - Row={Row} - /> + }> + + {...props} + label={VolumeSnapshotClassModel.labelPlural} + data={data} + loaded={loaded} + columns={columns} + getDataViewRows={getDataViewRows} + hideColumnManagement + /> + ); }; -const VolumeSnapshotClassPage: React.FC = ({ +const VolumeSnapshotClassPage: React.FCC = ({ canCreate = true, showTitle = true, namespace, selector, }) => { const { t } = useTranslation(); + const [resources, loaded, loadError] = useK8sWatchResource({ groupVersionKind: { group: VolumeSnapshotClassModel.apiGroup, @@ -125,26 +159,18 @@ const VolumeSnapshotClassPage: React.FC = ({ namespace, selector, }); - const [data, filteredData, onFilterChange] = useListPageFilter(resources); - const resourceKind = referenceForModel(VolumeSnapshotClassModel); return ( <> {canCreate && ( - + {t('console-app~Create VolumeSnapshotClass')} )} - - + ); @@ -159,8 +185,8 @@ type VolumeSnapshotClassPageProps = { type VolumeSnapshotClassTableProps = { data: VolumeSnapshotClassKind[]; - unfilteredData: VolumeSnapshotClassKind[]; loaded: boolean; loadError: unknown; }; + export default VolumeSnapshotClassPage; diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx index f226291eef..4c243a3883 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot-content.tsx @@ -1,22 +1,20 @@ import * as React from 'react'; -import { css } from '@patternfly/react-styles'; -import { sortable } from '@patternfly/react-table'; import { useTranslation } from 'react-i18next'; +import { + actionsCellProps, + cellIsStickyProps, + getNameCellProps, + ConsoleDataView, +} from '@console/app/src/components/data-view/ConsoleDataView'; +import { GetDataViewRows } from '@console/app/src/components/data-view/types'; import { ListPageBody, - useListPageFilter, ListPageCreate, - ListPageFilter, ListPageHeader, - VirtualizedTable, TableColumn, - RowProps, } from '@console/dynamic-plugin-sdk/src/lib-core'; -import { TableData } from '@console/internal/components/factory'; -import { useActiveColumns } from '@console/internal/components/factory/Table/active-columns-hook'; import type { PageComponentProps } from '@console/internal/components/utils/horizontal-nav'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; -import { Kebab } from '@console/internal/components/utils/kebab'; import { ResourceLink } from '@console/internal/components/utils/resource-link'; import { humanizeBinaryBytes } from '@console/internal/components/utils/units'; import { @@ -27,123 +25,159 @@ import { import { referenceForModel, VolumeSnapshotContentKind } from '@console/internal/module/k8s'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; +import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; import { Status } from '@console/shared/src/components/status/Status'; -import { snapshotStatusFilters, volumeSnapshotStatus } from '../../status'; +import { DASH } from '@console/shared/src/constants/ui'; +import { volumeSnapshotStatus } from '../../status'; + +const kind = referenceForModel(VolumeSnapshotContentModel); export const tableColumnInfo = [ { id: 'name' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-lg'), id: 'status' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-lg'), id: 'size' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-2xl'), id: 'volumeSnapshot' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-2xl'), id: 'snapshotClass' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-2xl'), id: 'createdAt' }, - { className: Kebab.columnClass, id: '' }, + { id: 'status' }, + { id: 'size' }, + { id: 'volumeSnapshot' }, + { id: 'snapshotClass' }, + { id: 'createdAt' }, + { id: '' }, ]; -const Row: React.FC> = ({ obj }) => { - const name = obj?.metadata?.name || ''; - const creationTimestamp = obj?.metadata?.creationTimestamp || ''; - const snapshotName = obj?.spec?.volumeSnapshotRef?.name || ''; - const snapshotNamespace = obj?.spec?.volumeSnapshotRef?.namespace || ''; - const size = obj.status?.restoreSize; - const sizeMetrics = size ? humanizeBinaryBytes(size).string : '-'; +const getDataViewRows: GetDataViewRows = (data, columns) => { + return data.map(({ obj }) => { + const name = obj.metadata?.name || ''; + const creationTimestamp = obj.metadata?.creationTimestamp || ''; + const snapshotName = obj.spec?.volumeSnapshotRef?.name || ''; + const snapshotNamespace = obj.spec?.volumeSnapshotRef?.namespace || ''; + const size = obj.status?.restoreSize; + const sizeMetrics = size ? humanizeBinaryBytes(size).string : DASH; - return ( - <> - - - - - - - {sizeMetrics} - - - - - - - - - - - - - - ); + const rowCells = { + [tableColumnInfo[0].id]: { + cell: , + props: getNameCellProps(name), + }, + [tableColumnInfo[1].id]: { + cell: , + }, + [tableColumnInfo[2].id]: { + cell: sizeMetrics, + }, + [tableColumnInfo[3].id]: { + cell: ( + + ), + }, + [tableColumnInfo[4].id]: { + cell: ( + + ), + }, + [tableColumnInfo[5].id]: { + cell: , + }, + [tableColumnInfo[6].id]: { + cell: , + props: { + ...actionsCellProps, + }, + }, + }; + + return columns.map(({ id }) => { + const cell = rowCells[id]?.cell || DASH; + const props = rowCells[id]?.props || undefined; + return { + id, + props, + cell, + }; + }); + }); }; -const VolumeSnapshotContentTable: React.FC = (props) => { +const useVolumeSnapshotContentColumns = (): TableColumn[] => { const { t } = useTranslation(); - const getTableColumns = (): TableColumn[] => [ - { - title: t('console-app~Name'), - sort: 'metadata.name', - transforms: [sortable], - id: tableColumnInfo[0].id, - }, - { - title: t('console-app~Status'), - sort: 'snapshotStatus', - transforms: [sortable], - props: { className: tableColumnInfo[1].className }, - id: tableColumnInfo[1].id, - }, - { - title: t('console-app~Size'), - sort: 'volumeSnapshotSize', - transforms: [sortable], - props: { className: tableColumnInfo[2].className }, - id: tableColumnInfo[2].id, - }, - { - title: t('console-app~VolumeSnapshot'), - sort: 'spec.volumeSnapshotRef.name', - transforms: [sortable], - props: { className: tableColumnInfo[3].className }, - id: tableColumnInfo[3].id, - }, - { - title: t('console-app~SnapshotClass'), - sort: 'spec.volumeSnapshotClassName', - transforms: [sortable], - props: { className: tableColumnInfo[4].className }, - id: tableColumnInfo[4].id, - }, - { - title: t('console-app~Created at'), - sort: 'metadata.creationTimestamp', - transforms: [sortable], - props: { className: tableColumnInfo[5].className }, - id: tableColumnInfo[5].id, - }, - { - title: '', - props: { className: tableColumnInfo[6].className }, - id: tableColumnInfo[6].id, - }, - ]; - const [columns] = useActiveColumns({ columns: getTableColumns() }); + + const columns: TableColumn[] = React.useMemo( + () => [ + { + title: t('console-app~Name'), + sort: 'metadata.name', + id: tableColumnInfo[0].id, + props: { ...cellIsStickyProps, modifier: 'nowrap' }, + }, + { + title: t('console-app~Status'), + sort: 'snapshotStatus', + id: tableColumnInfo[1].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Size'), + sort: 'volumeSnapshotSize', + id: tableColumnInfo[2].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~VolumeSnapshot'), + sort: 'spec.volumeSnapshotRef.name', + id: tableColumnInfo[3].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~SnapshotClass'), + sort: 'spec.volumeSnapshotClassName', + id: tableColumnInfo[4].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Created at'), + sort: 'metadata.creationTimestamp', + id: tableColumnInfo[5].id, + props: { modifier: 'nowrap' }, + }, + { + title: '', + id: tableColumnInfo[6].id, + props: { ...cellIsStickyProps }, + }, + ], + [t], + ); + + return columns; +}; + +const VolumeSnapshotContentTable: React.FCC = ({ + data, + loaded, + ...props +}) => { + const columns = useVolumeSnapshotContentColumns(); return ( - - {...props} - aria-label={t('console-app~VolumeSnapshotContents')} - label={t('console-app~VolumeSnapshotContents')} - columns={columns} - Row={Row} - /> + }> + + {...props} + label={VolumeSnapshotContentModel.labelPlural} + data={data} + loaded={loaded} + columns={columns} + getDataViewRows={getDataViewRows} + hideColumnManagement + /> + ); }; -const VolumeSnapshotContentPage: React.FC = ({ +const VolumeSnapshotContentPage: React.FCC = ({ showTitle = true, canCreate = true, }) => { @@ -158,30 +192,17 @@ const VolumeSnapshotContentPage: React.FC = ({ isList: true, }); - const [data, filteredData, onFilterChange] = useListPageFilter(resources); - const resourceKind = referenceForModel(VolumeSnapshotContentModel); return ( <> {canCreate && ( - + {t('console-app~Create VolumeSnapshotContent')} )} - - + ); @@ -193,7 +214,6 @@ type VolumeSnapshotContentPageProps = { type VolumeSnapshotContentTableProps = { data: VolumeSnapshotContentKind[]; - unfilteredData: VolumeSnapshotContentKind[]; loaded: boolean; loadError: unknown; }; diff --git a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx index 80c9494412..6dd7f5b453 100644 --- a/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx +++ b/frontend/packages/console-app/src/components/volume-snapshot/volume-snapshot.tsx @@ -1,23 +1,23 @@ import * as React from 'react'; -import { css } from '@patternfly/react-styles'; -import { sortable } from '@patternfly/react-table'; -import { TFunction } from 'i18next'; +import { DataViewCheckboxFilter } from '@patternfly/react-data-view'; +import { DataViewFilterOption } from '@patternfly/react-data-view/dist/cjs/DataViewFilters'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom-v5-compat'; +import { + actionsCellProps, + cellIsStickyProps, + getNameCellProps, + initialFiltersDefault, + ConsoleDataView, +} from '@console/app/src/components/data-view/ConsoleDataView'; +import { ResourceFilters, GetDataViewRows } from '@console/app/src/components/data-view/types'; import { ListPageBody, - useListPageFilter, - ListPageFilter, ListPageHeader, ListPageCreateLink, - VirtualizedTable, TableColumn, - RowProps, } from '@console/dynamic-plugin-sdk/src/lib-core'; -import { TableData } from '@console/internal/components/factory'; -import { useActiveColumns } from '@console/internal/components/factory/Table/active-columns-hook'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; -import { Kebab } from '@console/internal/components/utils/kebab'; import { ResourceLink } from '@console/internal/components/utils/resource-link'; import { convertToBaseValue, humanizeBinaryBytes } from '@console/internal/components/utils/units'; import { @@ -37,189 +37,273 @@ import { } from '@console/internal/module/k8s'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; +import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; import { Status } from '@console/shared/src/components/status/Status'; import { FLAGS } from '@console/shared/src/constants/common'; +import { DASH } from '@console/shared/src/constants/ui'; import { useFlag } from '@console/shared/src/hooks/flag'; import { getName, getNamespace } from '@console/shared/src/selectors/common'; import { snapshotSource } from '@console/shared/src/sorts/snapshot'; -import { snapshotStatusFilters, volumeSnapshotStatus } from '../../status'; +import { volumeSnapshotStatus } from '../../status'; + +const kind = referenceForModel(VolumeSnapshotModel); const tableColumnInfo = [ { id: 'name' }, { id: 'namespace' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-lg'), id: 'status' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-lg'), id: 'size' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-xl'), id: 'source' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-2xl'), id: 'snapshotContent' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-2xl'), id: 'snapshotClass' }, - { className: css('pf-m-hidden', 'pf-m-visible-on-xl'), id: 'createdAt' }, - { className: Kebab.columnClass, id: '' }, + { id: 'status' }, + { id: 'size' }, + { id: 'source' }, + { id: 'snapshotContent' }, + { id: 'snapshotClass' }, + { id: 'createdAt' }, + { id: '' }, ]; -const getTableColumns = (t: TFunction, disableItems = {}): TableColumn[] => - [ - { - title: t('console-app~Name'), - sort: 'metadata.name', - transforms: [sortable], - id: tableColumnInfo[0].id, - }, - { - title: t('console-app~Namespace'), - sort: 'metadata.namespace', - transforms: [sortable], - id: tableColumnInfo[1].id, - }, - { - title: t('console-app~Status'), - sort: 'snapshotStatus', - transforms: [sortable], - props: { className: tableColumnInfo[2].className }, - id: tableColumnInfo[2].id, - }, - { - title: t('console-app~Size'), - sort: 'volumeSnapshotSize', - transforms: [sortable], - props: { className: tableColumnInfo[3].className }, - id: tableColumnInfo[3].id, - }, - { - title: t('console-app~Source'), - sort: 'volumeSnapshotSource', - transforms: [sortable], - props: { className: tableColumnInfo[4].className }, - id: tableColumnInfo[4].id, - }, - { - title: t('console-app~Snapshot content'), - sort: 'status.boundVolumeSnapshotContentName', - transforms: [sortable], - props: { className: tableColumnInfo[5].className }, - id: tableColumnInfo[5].id, - }, - { - title: t('console-app~VolumeSnapshotClass'), - sort: 'spec.volumeSnapshotClassName', - transforms: [sortable], - props: { className: tableColumnInfo[6].className }, - id: tableColumnInfo[6].id, - }, - { - title: t('console-app~Created at'), - sort: 'metadata.creationTimestamp', - transforms: [sortable], - props: { className: tableColumnInfo[7].className }, - id: tableColumnInfo[7].id, - }, - { - title: '', - props: { className: tableColumnInfo[8].className }, - id: tableColumnInfo[8].id, - }, - ].filter((item) => !disableItems[item.title]); +const getDataViewRows: GetDataViewRows = ( + data, + columns, +) => { + return data.map(({ obj, rowData: { hideSnapshotContentColumn } }) => { + const name = obj.metadata?.name || ''; + const namespace = obj.metadata?.namespace || ''; + const creationTimestamp = obj.metadata?.creationTimestamp || ''; + const size = obj.status?.restoreSize; + const sizeBase = convertToBaseValue(size); + const sizeMetrics = size ? humanizeBinaryBytes(sizeBase).string : DASH; + const sourceModel = obj.spec?.source?.persistentVolumeClaimName + ? PersistentVolumeClaimModel + : VolumeSnapshotContentModel; + const sourceName = snapshotSource(obj); + const snapshotContent = obj.status?.boundVolumeSnapshotContentName; + const snapshotClass = obj.spec?.volumeSnapshotClassName; + const context = { [referenceFor(obj)]: obj }; -const Row: React.FC> = ({ - obj, - rowData: { customData }, -}) => { - const name = obj?.metadata?.name || ''; - const namespace = obj?.metadata?.namespace || ''; - const creationTimestamp = obj?.metadata?.creationTimestamp || ''; - const size = obj?.status?.restoreSize; - const sizeBase = convertToBaseValue(size); - const sizeMetrics = size ? humanizeBinaryBytes(sizeBase).string : '-'; - const sourceModel = obj?.spec?.source?.persistentVolumeClaimName - ? PersistentVolumeClaimModel - : VolumeSnapshotContentModel; - const sourceName = snapshotSource(obj); - const snapshotContent = obj?.status?.boundVolumeSnapshotContentName; - const snapshotClass = obj?.spec?.volumeSnapshotClassName; - const resourceKind = referenceFor(obj); - const context = { [resourceKind]: obj }; - return ( - <> - - - - - - - - - - {sizeMetrics} - {!customData?.disableItems?.Source && ( - + const rowCells = { + [tableColumnInfo[0].id]: { + cell: , + props: getNameCellProps(name), + }, + [tableColumnInfo[1].id]: { + cell: , + }, + [tableColumnInfo[2].id]: { + cell: , + }, + [tableColumnInfo[3].id]: { + cell: sizeMetrics, + }, + [tableColumnInfo[4].id]: { + cell: ( - - )} - {!customData?.disableItems?.['Snapshot Content'] && ( - - {snapshotContent ? ( - - ) : ( - '-' - )} - - )} - - {snapshotClass ? ( + ), + }, + [tableColumnInfo[5].id]: { + cell: snapshotContent ? ( + + ) : ( + DASH + ), + disabled: hideSnapshotContentColumn, + }, + [tableColumnInfo[6].id]: { + cell: snapshotClass ? ( ) : ( - '-' - )} - - - - - - - - + DASH + ), + }, + [tableColumnInfo[7].id]: { + cell: , + }, + [tableColumnInfo[8].id]: { + cell: , + props: { + ...actionsCellProps, + }, + }, + }; + + return columns + .filter(({ id }) => !rowCells[id].disabled) + .map(({ id }) => { + const cell = rowCells[id]?.cell || DASH; + const props = rowCells[id]?.props || undefined; + return { + id, + props, + cell, + }; + }); + }); +}; + +const useVolumeSnapshotColumns = ( + rowData: VolumeSnapshotRowData, +): TableColumn[] => { + const { t } = useTranslation(); + + const columns: TableColumn[] = React.useMemo( + () => + [ + { + title: t('console-app~Name'), + sort: 'metadata.name', + id: tableColumnInfo[0].id, + props: { ...cellIsStickyProps, modifier: 'nowrap' }, + }, + { + title: t('console-app~Namespace'), + sort: 'metadata.namespace', + id: tableColumnInfo[1].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Status'), + sort: 'snapshotStatus', + id: tableColumnInfo[2].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Size'), + sort: 'volumeSnapshotSize', + id: tableColumnInfo[3].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Source'), + sort: 'volumeSnapshotSource', + id: tableColumnInfo[4].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Snapshot content'), + sort: 'status.boundVolumeSnapshotContentName', + id: tableColumnInfo[5].id, + props: { modifier: 'nowrap' }, + disabled: rowData.hideSnapshotContentColumn, + }, + { + title: t('console-app~VolumeSnapshotClass'), + sort: 'spec.volumeSnapshotClassName', + id: tableColumnInfo[6].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('console-app~Created at'), + sort: 'metadata.creationTimestamp', + id: tableColumnInfo[7].id, + props: { modifier: 'nowrap' }, + }, + { + title: '', + id: tableColumnInfo[8].id, + props: { ...cellIsStickyProps }, + }, + ].filter((c) => !c.disabled), + [t, rowData.hideSnapshotContentColumn], ); + + return columns; }; -const VolumeSnapshotTable: React.FC = (props) => { +const VolumeSnapshotTable: React.FCC = ({ data, loaded, ...props }) => { const { t } = useTranslation(); + const canListVSC = useFlag(FLAGS.CAN_LIST_VSC); + + const customRowData: VolumeSnapshotRowData = { + hideSnapshotContentColumn: !canListVSC, + }; + + const columns = useVolumeSnapshotColumns(customRowData); + + const volumeSnapshotStatusFilterOptions = React.useMemo( + () => [ + { + value: 'Ready', + label: t('console-app~Ready'), + }, + { + value: 'Pending', + label: t('console-app~Pending'), + }, + { + value: 'Error', + label: t('console-app~Error'), + }, + ], + [t], + ); + + const initialFilters = React.useMemo( + () => ({ ...initialFiltersDefault, status: [] }), + [], + ); + + const additionalFilterNodes = React.useMemo( + () => [ + , + ], + [t, volumeSnapshotStatusFilterOptions], + ); - const columns = getTableColumns(t, props.rowData?.customData?.disableItems || {}); + const matchesAdditionalFilters = React.useCallback( + (resource: VolumeSnapshotKind, filters: VolumeSnapshotFilters) => { + // Status filter + if (filters.status.length > 0) { + const status = volumeSnapshotStatus(resource); + if (!filters.status.includes(status)) { + return false; + } + } - const [activeColumns] = useActiveColumns({ columns }); + return true; + }, + [], + ); return ( - - {...props} - data={props.data} - aria-label={t('console-app~VolumeSnapshots')} - label={t('console-app~VolumeSnapshots')} - columns={activeColumns} - Row={Row} - /> + }> + + {...props} + label={VolumeSnapshotModel.labelPlural} + data={data} + loaded={loaded} + columns={columns} + getDataViewRows={getDataViewRows} + customRowData={customRowData} + initialFilters={initialFilters} + additionalFilterNodes={additionalFilterNodes} + matchesAdditionalFilters={matchesAdditionalFilters} + hideColumnManagement + /> + ); }; -const VolumeSnapshotPage: React.FC = ({ +const VolumeSnapshotPage: React.FCC = ({ canCreate = true, showTitle = true, namespace, selector, }) => { const { t } = useTranslation(); - const canListVSC = useFlag(FLAGS.CAN_LIST_VSC); const createPath = `/k8s/ns/${namespace || 'default'}/${VolumeSnapshotModel.plural}/~new/form`; + const [resources, loaded, loadError] = useK8sWatchResource({ groupVersionKind: { group: VolumeSnapshotModel.apiGroup, @@ -231,7 +315,6 @@ const VolumeSnapshotPage: React.FC = ({ namespace, selector, }); - const [data, filteredData, onFilterChange] = useListPageFilter(resources); return ( <> @@ -243,59 +326,26 @@ const VolumeSnapshotPage: React.FC = ({ )} - - + ); }; -const checkPVCSnapshot: CheckPVCSnapshot = (volumeSnapshots, pvc) => +const checkPVCSnapshot = ( + volumeSnapshots: VolumeSnapshotKind[], + pvc: K8sResourceKind, +): VolumeSnapshotKind[] => volumeSnapshots?.filter( (snapshot) => snapshot?.spec?.source?.persistentVolumeClaimName === getName(pvc) && getNamespace(snapshot) === getNamespace(pvc), ); -const FilteredSnapshotTable: React.FC = (props) => { - const { t } = useTranslation(); - const { data, rowData } = props; - const columns = getTableColumns(t, props.rowData?.customData?.disableItems || {}); - const [activeColumns] = useActiveColumns({ - columns, - }); - return ( - - {...props} - data={checkPVCSnapshot(data, rowData.customData.pvc)} - aria-label={t('console-app~VolumeSnapshots')} - label={t('console-app~VolumeSnapshots')} - columns={activeColumns} - Row={Row} - /> - ); -}; - -export const VolumeSnapshotPVCPage: React.FC = ({ ns, obj }) => { - const { t } = useTranslation(); +export const VolumeSnapshotPVCPage: React.FCC = ({ ns, obj }) => { const params = useParams(); - const canListVSC = useFlag(FLAGS.CAN_LIST_VSC); const namespace = ns || params?.ns; + const [resources, loaded, loadError] = useK8sWatchResource({ groupVersionKind: { group: VolumeSnapshotModel.apiGroup, @@ -306,31 +356,22 @@ export const VolumeSnapshotPVCPage: React.FC = ({ ns, obj namespaced: true, namespace, }); - const [data, filteredData, onFilterChange] = useListPageFilter(resources); return ( - - ); }; + +type VolumeSnapshotFilters = ResourceFilters & { + status: string[]; +}; + type VolumeSnapshotPageProps = { namespace?: string; canCreate?: boolean; @@ -338,24 +379,6 @@ type VolumeSnapshotPageProps = { selector?: Selector; }; -type CheckPVCSnapshot = ( - volumeSnapshots: VolumeSnapshotKind[], - pvc: K8sResourceKind, -) => VolumeSnapshotKind[]; - -type FilteredSnapshotTable = { - data: VolumeSnapshotKind[]; - unfilteredData: VolumeSnapshotKind[]; - rowData: { - customData: { - disableItems?: Record; - pvc: PersistentVolumeClaimKind; - }; - }; - loaded: boolean; - loadError: unknown; -}; - type VolumeSnapshotPVCPage = { obj: PersistentVolumeClaimKind; ns: string; @@ -363,21 +386,12 @@ type VolumeSnapshotPVCPage = { type VolumeSnapshotTableProps = { data: VolumeSnapshotKind[]; - unfilteredData: VolumeSnapshotKind[]; - rowData?: { - customData?: { - disableItems?: Record; - }; - }; loaded: boolean; loadError: unknown; }; -type VolumeSnapshotRowProsCustomData = { - customData?: { - disableItems?: Record; - pvc?: PersistentVolumeClaimKind; - }; +type VolumeSnapshotRowData = { + hideSnapshotContentColumn?: boolean; }; export default VolumeSnapshotPage; diff --git a/frontend/public/components/persistent-volume-claim.tsx b/frontend/public/components/persistent-volume-claim.tsx index 9a2b7a59bd..b600e99912 100644 --- a/frontend/public/components/persistent-volume-claim.tsx +++ b/frontend/public/components/persistent-volume-claim.tsx @@ -1,10 +1,28 @@ -import * as React from 'react'; import * as _ from 'lodash-es'; -import { css } from '@patternfly/react-styles'; -import { useDispatch, useSelector } from 'react-redux'; -import { sortable } from '@patternfly/react-table'; +import * as React from 'react'; +import i18next from 'i18next'; import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, +} from '@patternfly/react-core'; +import { DataViewCheckboxFilter } from '@patternfly/react-data-view'; +import { DataViewFilterOption } from '@patternfly/react-data-view/dist/cjs/DataViewFilters'; import { ChartDonut } from '@patternfly/react-charts/victory'; +import { + actionsCellProps, + cellIsStickyProps, + getNameCellProps, + initialFiltersDefault, + ConsoleDataView, +} from '@console/app/src/components/data-view/ConsoleDataView'; +import { ResourceFilters, GetDataViewRows } from '@console/app/src/components/data-view/types'; +import { TableColumn } from '@console/dynamic-plugin-sdk/src/lib-core'; import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; import { isPVCAlert, @@ -14,9 +32,12 @@ import { PVCAlert, } from '@console/dynamic-plugin-sdk/src/extensions/pvc'; import { useResolvedExtensions } from '@console/dynamic-plugin-sdk'; +import { PersistentVolumeClaimKind, referenceFor } from '@console/internal/module/k8s'; +import { RootState } from '@console/internal/redux'; import ActionServiceProvider from '@console/shared/src/components/actions/ActionServiceProvider'; import ActionMenu from '@console/shared/src/components/actions/menu/ActionMenu'; import { ActionMenuVariant } from '@console/shared/src/components/actions/types'; +import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; import { Status } from '@console/shared/src/components/status/Status'; import { FLAGS } from '@console/shared/src/constants/common'; import { calculateRadius } from '@console/shared/src/utils/pod-utils'; @@ -24,13 +45,11 @@ import { getNamespace, getName } from '@console/shared/src/selectors/common'; import { getRequestedPVCSize } from '@console/shared/src/selectors/storage'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; import { useFlag } from '@console/shared/src/hooks/flag'; -import { PersistentVolumeClaimKind, referenceFor } from '@console/internal/module/k8s'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; +import { DASH } from '@console/shared/src/constants/ui'; import { Conditions } from './conditions'; import { DetailsPage } from './factory/details'; import { ListPage } from './factory/list-page'; -import { Table, TableData } from './factory/table'; -import { Kebab } from './utils/kebab'; import { navFactory } from './utils/horizontal-nav'; import { SectionHeading } from './utils/headings'; import { ResourceLink } from './utils/resource-link'; @@ -39,27 +58,33 @@ import { Selector } from './utils/selector'; import { humanizeBinaryBytes, convertToBaseValue } from './utils/units'; import { ResourceEventStream } from './events'; import { PVCMetrics, setPVCMetrics } from '@console/internal/actions/ui'; +import { PersistentVolumeClaimModel } from '@console/internal/models'; import { PrometheusEndpoint } from './graphs/helpers'; -import { RootState } from '@console/internal/redux'; import { usePrometheusPoll } from './graphs/prometheus-poll-hook'; -import i18next from 'i18next'; -import { - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - Grid, - GridItem, -} from '@patternfly/react-core'; import { VolumeAttributesClassModel } from '../models'; -export const PVCStatusComponent: React.FC = ({ pvc }) => { +const { kind } = PersistentVolumeClaimModel; + +const tableColumnInfo = [ + { id: 'name' }, + { id: 'namespace' }, + { id: 'status' }, + { id: 'persistentVolume' }, + { id: 'totalCapacity' }, + { id: 'usedCapacity' }, + { id: 'storageClass' }, + { id: '' }, +]; + +export const PVCStatusComponent: React.FCC = ({ pvc }) => { const { t } = useTranslation(); const [pvcStatusExtensions, resolved] = useResolvedExtensions(isPVCStatus); + if (resolved && pvcStatusExtensions.length > 0) { const sortedByPriority = pvcStatusExtensions.sort( (a, b) => b.properties.priority - a.properties.priority, ); + const priorityStatus = sortedByPriority.find((status) => status.properties.predicate(pvc)); const PriorityStatusComponent = priorityStatus?.properties?.status; @@ -77,43 +102,33 @@ export const PVCStatusComponent: React.FC = ({ pvc }) => { ); }; -const tableColumnClasses = [ - '', // name - '', // namespace - css('pf-m-hidden', 'pf-m-visible-on-lg'), // status - css('pf-m-hidden', 'pf-m-visible-on-xl'), // persistence volume - css('pf-m-hidden', 'pf-m-visible-on-xl'), // capacity - css('pf-m-hidden', 'pf-m-visible-on-2xl'), // used capacity - css('pf-m-hidden', 'pf-m-visible-on-2xl'), // storage class - Kebab.columnClass, -]; +const getDataViewRows: GetDataViewRows = (data, columns) => { + /* eslint-disable react-hooks/rules-of-hooks */ + const { t } = useTranslation(); + const pvcMetrics = useSelector(({ UI }) => UI.getIn(['metrics', 'pvc'])); + /* eslint-enable react-hooks/rules-of-hooks */ -const kind = 'PersistentVolumeClaim'; + return data.map(({ obj }) => { + const metrics = pvcMetrics?.usedCapacity?.[getNamespace(obj)]?.[getName(obj)]; + const [name, namespace] = [getName(obj), getNamespace(obj)]; + const totalCapacityMetric = convertToBaseValue(obj.status?.capacity?.storage); + const totalCapcityHumanized = humanizeBinaryBytes(totalCapacityMetric); + const usedCapacity = humanizeBinaryBytes(metrics); + const context = { [referenceFor(obj)]: obj }; -const PVCTableRow: React.FC = ({ obj }) => { - const metrics = useSelector( - ({ UI }) => UI.getIn(['metrics', 'pvc'])?.usedCapacity?.[getNamespace(obj)]?.[getName(obj)], - ); - const [name, namespace] = [getName(obj), getNamespace(obj)]; - const totalCapacityMetric = convertToBaseValue(obj?.status?.capacity?.storage); - const totalCapcityHumanized = humanizeBinaryBytes(totalCapacityMetric); - const usedCapacity = humanizeBinaryBytes(metrics); - const { t } = useTranslation(); - const resourceKind = referenceFor(obj); - const context = { [resourceKind]: obj }; - return ( - <> - - - - - - - - - - - {_.get(obj, 'spec.volumeName') ? ( + const rowCells = { + [tableColumnInfo[0].id]: { + cell: , + props: getNameCellProps(name), + }, + [tableColumnInfo[1].id]: { + cell: , + }, + [tableColumnInfo[2].id]: { + cell: , + }, + [tableColumnInfo[3].id]: { + cell: obj.spec?.volumeName ? ( = ({ obj }) => { /> ) : (
{t('public~No PersistentVolume')}
- )} -
- - {totalCapacityMetric ? totalCapcityHumanized.string : '-'} - - {metrics ? usedCapacity.string : '-'} - - {obj?.spec?.storageClassName ? ( + ), + }, + [tableColumnInfo[4].id]: { + cell: totalCapacityMetric ? totalCapcityHumanized.string : DASH, + }, + [tableColumnInfo[5].id]: { + cell: metrics ? usedCapacity.string : DASH, + }, + [tableColumnInfo[6].id]: { + cell: obj.spec?.storageClassName ? ( ) : ( - '-' - )} - - - - - + DASH + ), + }, + [tableColumnInfo[7].id]: { + cell: , + props: { + ...actionsCellProps, + }, + }, + }; + + return columns.map(({ id }) => { + const cell = rowCells[id]?.cell || DASH; + const props = rowCells[id]?.props || undefined; + return { + id, + props, + cell, + }; + }); + }); +}; + +const usePersistentVolumeClaimColumns = (): TableColumn[] => { + const { t } = useTranslation(); + + const columns: TableColumn[] = React.useMemo( + () => [ + { + title: t('public~Name'), + sort: 'metadata.name', + id: tableColumnInfo[0].id, + props: { ...cellIsStickyProps, modifier: 'nowrap' }, + }, + { + title: t('public~Namespace'), + sort: 'metadata.namespace', + id: tableColumnInfo[1].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Status'), + sort: 'status.phase', + id: tableColumnInfo[2].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~PersistentVolume'), + sort: 'spec.volumeName', + id: tableColumnInfo[3].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Capacity'), + sort: 'pvcStorage', + id: tableColumnInfo[4].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Used'), + sort: 'pvcUsed', + id: tableColumnInfo[5].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~StorageClass'), + sort: 'spec.storageClassName', + id: tableColumnInfo[6].id, + props: { modifier: 'nowrap' }, + }, + { + title: '', + id: tableColumnInfo[7].id, + props: { ...cellIsStickyProps }, + }, + ], + [t], ); + + return columns; }; -const Details: React.FC = ({ obj: pvc }) => { +const PVCDetails: React.FCC = ({ obj: pvc }) => { const flags = useFlag(FLAGS.CAN_LIST_PV); + const { t } = useTranslation(); + const canListPV = flags[FLAGS.CAN_LIST_PV]; const isVACSupported = useFlag(FLAGS.VAC_PLATFORM_SUPPORT); const name = pvc?.metadata?.name; @@ -160,10 +251,12 @@ const Details: React.FC = ({ obj: pvc }) => { const accessModes = pvc?.status?.accessModes; const volumeMode = pvc?.spec?.volumeMode; const conditions = pvc?.status?.conditions; + const query = name && namespace ? `kubelet_volume_stats_used_bytes{persistentvolumeclaim='${name}',namespace='${namespace}'}` : ''; + const [response, loadError, loading] = usePrometheusPoll({ endpoint: PrometheusEndpoint.QUERY, namespace, @@ -178,9 +271,11 @@ const Details: React.FC = ({ obj: pvc }) => { const availableCapacity = humanizeBinaryBytes(availableMetrics, undefined, totalCapacity.unit); const usedCapacity = humanizeBinaryBytes(usedMetrics, undefined, totalCapacity.unit); const { podStatusInnerRadius: innerRadius, podStatusOuterRadius: radius } = calculateRadius(130); + const availableCapacityString = `${Number(availableCapacity.value.toFixed(1))} ${ availableCapacity.unit }`; + const totalCapacityString = `${Number(totalCapacity.value.toFixed(1))} ${totalCapacity.unit}`; const donutData = usedMetrics @@ -191,10 +286,11 @@ const Details: React.FC = ({ obj: pvc }) => { : [{ x: i18next.t('public~Total'), y: totalCapacity.value }]; const [pvcAlertExtensions] = useResolvedExtensions(isPVCAlert); + const alertComponents = pvcAlertExtensions?.map( ({ properties: { alert: AlertComponent }, uid }) => , ); - const { t } = useTranslation(); + return ( <> @@ -286,7 +382,7 @@ const Details: React.FC = ({ obj: pvc }) => { {storageClassName ? ( ) : ( - '-' + DASH )} @@ -321,80 +417,97 @@ const Details: React.FC = ({ obj: pvc }) => { ); }; -export const PersistentVolumeClaimsList = (props) => { +export const PersistentVolumeClaimList: React.FCC = ({ + data, + loaded, + ...props +}) => { const { t } = useTranslation(); - const PVCTableHeader = () => { - return [ - { - title: t('public~Name'), - sortField: 'metadata.name', - transforms: [sortable], - props: { className: tableColumnClasses[0] }, - }, - { - title: t('public~Namespace'), - sortField: 'metadata.namespace', - transforms: [sortable], - props: { className: tableColumnClasses[1] }, - id: 'namespace', - }, - { - title: t('public~Status'), - sortField: 'status.phase', - transforms: [sortable], - props: { className: tableColumnClasses[2] }, - }, - { - title: t('public~PersistentVolumes'), - sortField: 'spec.volumeName', - transforms: [sortable], - props: { className: tableColumnClasses[3] }, - }, - { - title: t('public~Capacity'), - sortFunc: 'pvcStorage', - transforms: [sortable], - props: { className: tableColumnClasses[4] }, - }, + const columns = usePersistentVolumeClaimColumns(); + + const pvcStatusFilterOptions = React.useMemo( + () => [ { - title: t('public~Used'), - sortFunc: 'pvcUsed', - transforms: [sortable], - props: { className: tableColumnClasses[5] }, + value: 'Pending', + label: t('public~Pending'), }, { - sortField: 'spec.storageClassName', - title: t('public~StorageClass'), - transforms: [sortable], - props: { className: tableColumnClasses[6] }, + value: 'Bound', + label: t('public~Bound'), }, { - title: '', - props: { className: tableColumnClasses[7] }, + value: 'Lost', + label: t('public~Lost'), }, - ]; - }; + ], + [t], + ); + + const initialFilters = React.useMemo( + () => ({ ...initialFiltersDefault, status: [] }), + [], + ); + + const additionalFilterNodes = React.useMemo( + () => [ + , + ], + [t, pvcStatusFilterOptions], + ); + + const matchesAdditionalFilters = React.useCallback( + (resource: PersistentVolumeClaimKind, filters: PersistentVolumeClaimFilters) => { + // Status filter + if (filters.status.length > 0) { + const status = resource.status.phase; + if (!filters.status.includes(status)) { + return false; + } + } + + return true; + }, + [], + ); + return ( - + }> + + {...props} + label={t('public~PersistentVolumeClaims')} + data={data} + loaded={loaded} + columns={columns} + getDataViewRows={getDataViewRows} + initialFilters={initialFilters} + additionalFilterNodes={additionalFilterNodes} + matchesAdditionalFilters={matchesAdditionalFilters} + hideColumnManagement + /> + ); }; -export const PersistentVolumeClaimsPage = (props) => { +export const PersistentVolumeClaimsPage: React.FCC = ({ + namespace, + ...props +}) => { const { t } = useTranslation(); const createPropExtensions = useExtensions(isPVCCreateProp); - const { namespace = undefined } = props; const dispatch = useDispatch(); + const [response, loadError, loading] = usePrometheusPoll({ endpoint: PrometheusEndpoint.QUERY, namespace, query: 'kubelet_volume_stats_used_bytes', }); + const pvcMetrics = _.isEmpty(loadError) && !loading ? response?.data?.result?.reduce((acc, item) => { @@ -406,8 +519,10 @@ export const PersistentVolumeClaimsPage = (props) => { return acc; }, {}) : {}; + dispatch(setPVCMetrics(pvcMetrics)); - const initPath = `/k8s/ns/${props.namespace || 'default'}/persistentvolumeclaims/`; + + const initPath = `/k8s/ns/${namespace || 'default'}/persistentvolumeclaims/`; const createItems = createPropExtensions.map(({ properties: { label, path } }, i) => ({ key: i + 1, @@ -432,27 +547,14 @@ export const PersistentVolumeClaimsPage = (props) => { }, }; - const allPhases = ['Pending', 'Bound', 'Lost']; - - const filters = [ - { - filterGroupName: t('public~Status'), - type: 'pvc-status', - reducer: (pvc) => pvc.status.phase, - items: _.map(allPhases, (phase) => ({ - id: phase, - title: phase, - })), - }, - ]; - return ( @@ -483,7 +585,7 @@ export const PersistentVolumeClaimsDetailsPage = (props) => { } customActionMenu={customActionMenu} pages={[ - navFactory.details(Details), + navFactory.details(PVCDetails), navFactory.editYaml(), navFactory.events(ResourceEventStream), ]} @@ -491,7 +593,21 @@ export const PersistentVolumeClaimsDetailsPage = (props) => { ); }; -type PVCTableRowProps = { obj: PersistentVolumeClaimKind }; +type PersistentVolumeClaimFilters = ResourceFilters & { + status: string[]; +}; + +type PersistentVolumeClaimListProps = { + data: PersistentVolumeClaimKind[]; + loaded: boolean; + loadError: unknown; +}; + +type PersistentVolumeClaimsPageProps = { + namespace?: string; + canCreate?: boolean; + showTitle?: boolean; +}; type PVCStatusProps = { pvc: PersistentVolumeClaimKind }; diff --git a/frontend/public/components/persistent-volume.tsx b/frontend/public/components/persistent-volume.tsx index 7ca55d8299..01726b7155 100644 --- a/frontend/public/components/persistent-volume.tsx +++ b/frontend/public/components/persistent-volume.tsx @@ -1,64 +1,78 @@ import * as _ from 'lodash-es'; -import { sortable } from '@patternfly/react-table'; -import { Status } from '@console/shared/src/components/status/Status'; +import * as React from 'react'; import { useTranslation } from 'react-i18next'; - +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, +} from '@patternfly/react-core'; +import { + actionsCellProps, + cellIsStickyProps, + getNameCellProps, + ConsoleDataView, +} from '@console/app/src/components/data-view/ConsoleDataView'; +import { GetDataViewRows } from '@console/app/src/components/data-view/types'; +import { TableColumn } from '@console/dynamic-plugin-sdk/src/lib-core'; +import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; +import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; +import { Status } from '@console/shared/src/components/status/Status'; +import { DASH } from '@console/shared/src/constants/ui'; import { DetailsPage } from './factory/details'; import { ListPage } from './factory/list-page'; -import { Table, TableData } from './factory/table'; import type { DetailsPageProps } from './factory/details'; import type { ListPageProps } from './factory/list-page'; -import type { TableProps } from './factory/table'; -import { Kebab } from './utils/kebab'; import { LabelList } from './utils/label-list'; import { navFactory } from './utils/horizontal-nav'; import { SectionHeading } from './utils/headings'; import { ResourceLink } from './utils/resource-link'; import { ResourceSummary } from './utils/details-page'; -import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; import { PersistentVolumeModel } from '../models'; -import { - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - Grid, - GridItem, -} from '@patternfly/react-core'; import { PersistentVolumeKind, referenceForModel } from '@console/internal/module/k8s'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; +const { kind } = PersistentVolumeModel; const persistentVolumeReference = referenceForModel(PersistentVolumeModel); +const tableColumnInfo = [ + { id: 'name' }, + { id: 'status' }, + { id: 'claim' }, + { id: 'capacity' }, + { id: 'labels' }, + { id: 'created' }, + { id: '' }, +]; + const PVStatus = ({ pv }: { pv: PersistentVolumeKind }) => ( ); -const tableColumnClasses = [ - '', - 'pf-m-hidden pf-m-visible-on-md', - 'pf-m-hidden pf-m-visible-on-lg', - 'pf-m-hidden pf-m-visible-on-lg', - '', - 'pf-m-hidden pf-m-visible-on-lg', - Kebab.columnClass, -]; +const getDataViewRows: GetDataViewRows = (data, columns) => { + /* eslint-disable react-hooks/rules-of-hooks */ + const { t } = useTranslation(); + /* eslint-enable react-hooks/rules-of-hooks */ -const { kind } = PersistentVolumeModel; + return data.map(({ obj }) => { + const name = obj.metadata?.name || ''; + const namespace = obj.metadata?.namespace || ''; + const labels = obj.metadata?.labels || {}; + const creationTimestamp = obj.metadata?.creationTimestamp || ''; -const PVTableRow = ({ obj }: { obj: PersistentVolumeKind }) => { - const { t } = useTranslation(); - return ( - <> - - - - - - - - {_.get(obj, 'spec.claimRef.name') ? ( + const rowCells = { + [tableColumnInfo[0].id]: { + cell: , + props: getNameCellProps(name), + }, + [tableColumnInfo[1].id]: { + cell: , + }, + [tableColumnInfo[2].id]: { + cell: obj.spec?.claimRef?.name ? ( { /> ) : (
{t('public~No claim')}
- )} -
- - {_.get(obj, 'spec.capacity.storage', '-')} - - - - - - - - - - - + ), + }, + [tableColumnInfo[3].id]: { + cell: _.get(obj, 'spec.capacity.storage', DASH), + }, + [tableColumnInfo[4].id]: { + cell: , + }, + [tableColumnInfo[5].id]: { + cell: , + }, + [tableColumnInfo[6].id]: { + cell: , + props: { + ...actionsCellProps, + }, + }, + }; + + return columns.map(({ id }) => { + const cell = rowCells[id]?.cell || DASH; + const props = rowCells[id]?.props || undefined; + return { + id, + props, + cell, + }; + }); + }); +}; + +const usePersistentVolumeColumns = (): TableColumn[] => { + const { t } = useTranslation(); + + const columns: TableColumn[] = React.useMemo( + () => [ + { + title: t('public~Name'), + sort: 'metadata.name', + id: tableColumnInfo[0].id, + props: { ...cellIsStickyProps, modifier: 'nowrap' }, + }, + { + title: t('public~Status'), + sort: 'status.phase', + id: tableColumnInfo[1].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Claim'), + sort: 'spec.claimRef.name', + id: tableColumnInfo[2].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Capacity'), + sort: 'pvStorage', + id: tableColumnInfo[3].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Labels'), + sort: 'metadata.labels', + id: tableColumnInfo[4].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Created'), + sort: 'metadata.creationTimestamp', + id: tableColumnInfo[5].id, + props: { modifier: 'nowrap' }, + }, + { + title: '', + id: tableColumnInfo[6].id, + props: { ...cellIsStickyProps }, + }, + ], + [t], ); + + return columns; }; -const Details = ({ obj: pv }: { obj: PersistentVolumeKind }) => { +const PVDetails = ({ obj: pv }: { obj: PersistentVolumeKind }) => { const { t } = useTranslation(); const storageClassName = pv.spec?.storageClassName; const pvcName = pv.spec?.claimRef?.name; @@ -165,70 +245,53 @@ const Details = ({ obj: pv }: { obj: PersistentVolumeKind }) => { ); }; -export const PersistentVolumesList = (props: Partial) => { +export const PersistentVolumeList: React.FCC = ({ + data, + loaded, + ...props +}) => { + const columns = usePersistentVolumeColumns(); + + return ( + }> + + {...props} + label={PersistentVolumeModel.labelPlural} + data={data} + loaded={loaded} + columns={columns} + getDataViewRows={getDataViewRows} + hideColumnManagement + /> + + ); +}; + +export const PersistentVolumesPage = (props: ListPageProps) => { const { t } = useTranslation(); - const PVTableHeader = () => { - return [ - { - title: t('public~Name'), - sortField: 'metadata.name', - transforms: [sortable], - props: { className: tableColumnClasses[0] }, - }, - { - title: t('public~Status'), - sortField: 'status.phase', - transforms: [sortable], - props: { className: tableColumnClasses[1] }, - }, - { - title: t('public~Claim'), - sortField: 'spec.claimRef.name', - transforms: [sortable], - props: { className: tableColumnClasses[2] }, - }, - { - title: t('public~Capacity'), - sortFunc: 'pvStorage', - transforms: [sortable], - props: { className: tableColumnClasses[3] }, - }, - { - title: t('public~Labels'), - sortField: 'metadata.labels', - transforms: [sortable], - props: { className: tableColumnClasses[4] }, - }, - { - title: t('public~Created'), - sortField: 'metadata.creationTimestamp', - transforms: [sortable], - props: { className: tableColumnClasses[5] }, - }, - { - title: '', - props: { className: tableColumnClasses[6] }, - }, - ]; - }; + return ( -
); }; -export const PersistentVolumesPage = (props: ListPageProps) => ( - -); export const PersistentVolumesDetailsPage = (props: DetailsPageProps) => ( ); + +type PersistentVolumeListProps = { + data: PersistentVolumeKind[]; + loaded: boolean; + loadError: unknown; +}; diff --git a/frontend/public/components/storage-class.tsx b/frontend/public/components/storage-class.tsx index a63536bfb1..c0e1689d5a 100644 --- a/frontend/public/components/storage-class.tsx +++ b/frontend/public/components/storage-class.tsx @@ -1,19 +1,33 @@ -import * as React from 'react'; import * as _ from 'lodash-es'; -import { sortable } from '@patternfly/react-table'; +import * as React from 'react'; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, +} from '@patternfly/react-core'; +import { + actionsCellProps, + cellIsStickyProps, + getNameCellProps, + ConsoleDataView, +} from '@console/app/src/components/data-view/ConsoleDataView'; +import { GetDataViewRows } from '@console/app/src/components/data-view/types'; +import { TableColumn } from '@console/dynamic-plugin-sdk/src/lib-core'; +import { useTranslation } from 'react-i18next'; +import { StorageClassModel } from '@console/internal/models'; import ActionMenu from '@console/shared/src/components/actions/menu/ActionMenu'; import { ActionMenuVariant } from '@console/shared/src/components/actions/types'; import ActionServiceProvider from '@console/shared/src/components/actions/ActionServiceProvider'; import LazyActionMenu from '@console/shared/src/components/actions/LazyActionMenu'; -import { useTranslation } from 'react-i18next'; -import { css } from '@patternfly/react-styles'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; +import { LoadingBox } from '@console/shared/src/components/loading/LoadingBox'; +import { DASH } from '@console/shared/src/constants/ui'; import { DetailsPage } from './factory/details'; import { ListPage } from './factory/list-page'; -import { Table, TableData } from './factory/table'; -import type { RowFunctionArgs } from './factory/table'; import { DetailsItem } from './utils/details-item'; -import { Kebab } from './utils/kebab'; import { ResourceLink } from './utils/resource-link'; import { ResourceSummary, detailsPage } from './utils/details-page'; import { SectionHeading } from './utils/headings'; @@ -21,25 +35,23 @@ import { navFactory } from './utils/horizontal-nav'; import { StorageClassResourceKind, K8sResourceKind, - K8sResourceKindReference, referenceFor, referenceForModel, } from '../module/k8s'; -import { - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - Grid, - GridItem, -} from '@patternfly/react-core'; -export const StorageClassReference: K8sResourceKindReference = 'StorageClass'; +const { kind } = StorageClassModel; export const defaultClassAnnotation = 'storageclass.kubernetes.io/is-default-class'; const betaDefaultStorageClassAnnotation = 'storageclass.beta.kubernetes.io/is-default-class'; const defaultVirtClassAnnotation = 'storageclass.kubevirt.io/is-default-virt-class'; +const tableColumnInfo = [ + { id: 'name' }, + { id: 'provisioner' }, + { id: 'reclaimPolicy' }, + { id: '' }, +]; + export const isDefaultClass = (storageClass: K8sResourceKind) => { const annotations = _.get(storageClass, 'metadata.annotations') || {}; return ( @@ -53,15 +65,119 @@ const isDefaultVirtClass = (storageClass: K8sResourceKind) => { return annotations[defaultVirtClassAnnotation] === 'true'; }; -const tableColumnClasses = [ - 'pf-v6-u-w-42-on-md', - 'pf-v6-u-w-42-on-md', - 'pf-m-hidden pf-m-visible-on-md pf-v6-u-w-16-on-md', - Kebab.columnClass, -]; +const getDataViewRows: GetDataViewRows = (data, columns) => { + /* eslint-disable react-hooks/rules-of-hooks */ + const { t } = useTranslation(); + /* eslint-enable react-hooks/rules-of-hooks */ -const StorageClassDetails: React.FC = ({ obj }) => { + const isKubevirtPluginActive = + Array.isArray(window.SERVER_FLAGS.consolePlugins) && + window.SERVER_FLAGS.consolePlugins.includes('kubevirt-plugin'); + + return data.map(({ obj }) => { + const name = obj.metadata?.name || ''; + const context = { [referenceFor(obj)]: obj }; + + const rowCells = { + [tableColumnInfo[0].id]: { + cell: ( + + {isDefaultClass(obj) && ( + + – {t('public~Default')} + + )} + {isDefaultVirtClass(obj) && isKubevirtPluginActive && ( + + – {t('public~Default for VirtualMachines')} + + )} + + ), + props: getNameCellProps(name), + }, + [tableColumnInfo[1].id]: { + cell: obj.provisioner, + }, + [tableColumnInfo[2].id]: { + cell: obj.reclaimPolicy || DASH, + }, + [tableColumnInfo[3].id]: { + cell: , + props: { + ...actionsCellProps, + }, + }, + }; + + return columns.map(({ id }) => { + const cell = rowCells[id]?.cell || DASH; + const props = rowCells[id]?.props || undefined; + return { + id, + props, + cell, + }; + }); + }); +}; + +const useStorageClassColumns = (): TableColumn[] => { const { t } = useTranslation(); + + const columns: TableColumn[] = React.useMemo( + () => [ + { + title: t('public~Name'), + sort: 'metadata.name', + id: tableColumnInfo[0].id, + props: { ...cellIsStickyProps, modifier: 'nowrap' }, + }, + { + title: t('public~Provisioner'), + sort: 'provisioner', + id: tableColumnInfo[1].id, + props: { modifier: 'nowrap' }, + }, + { + title: t('public~Reclaim policy'), + sort: 'reclaimPolicy', + id: tableColumnInfo[2].id, + props: { modifier: 'nowrap' }, + }, + { + title: '', + id: tableColumnInfo[3].id, + props: { ...cellIsStickyProps }, + }, + ], + [t], + ); + + return columns; +}; + +export const StorageClassList: React.FCC = ({ data, loaded, ...props }) => { + const columns = useStorageClassColumns(); + + return ( + }> + + {...props} + label={StorageClassModel.labelPlural} + data={data} + loaded={loaded} + columns={columns} + getDataViewRows={getDataViewRows} + hideColumnManagement + /> + + ); +}; + +const StorageClassDetails: React.FCC = ({ obj }) => { + const { t } = useTranslation(); + return ( @@ -92,101 +208,30 @@ const StorageClassDetails: React.FC = ({ obj }) => { ); }; -const StorageClassTableRow: React.FC> = ({ obj }) => { +export const StorageClassPage: React.FCC = ({ ...props }) => { const { t } = useTranslation(); - const isKubevirtPluginActive = - Array.isArray(window.SERVER_FLAGS.consolePlugins) && - window.SERVER_FLAGS.consolePlugins.includes('kubevirt-plugin'); - - const resourceKind = referenceFor(obj); - const context = { [resourceKind]: obj }; - return ( - <> - - - {isDefaultClass(obj) && ( - - – {t('public~Default')} - - )} - {isDefaultVirtClass(obj) && isKubevirtPluginActive && ( - - – {t('public~Default for VirtualMachines')} - - )} - - - - {obj.provisioner} - - {obj.reclaimPolicy || '-'} - - - - - ); -}; - -export const StorageClassList: React.FC = (props) => { - const { t } = useTranslation(); - const StorageClassTableHeader = () => { - return [ - { - title: t('public~Name'), - sortField: 'metadata.name', - transforms: [sortable], - props: { className: tableColumnClasses[0] }, - }, - { - title: t('public~Provisioner'), - sortField: 'provisioner', - transforms: [sortable], - props: { className: tableColumnClasses[1] }, - }, - { - title: t('public~Reclaim policy'), - sortField: 'reclaimPolicy', - transforms: [sortable], - props: { className: tableColumnClasses[2] }, - }, - { - title: '', - props: { className: tableColumnClasses[3] }, - }, - ]; - }; - return ( -
- ); -}; -StorageClassList.displayName = 'StorageClassList'; -export const StorageClassPage: React.FC = (props) => { const createProps = { to: '/k8s/cluster/storageclasses/~new/form', }; - const { t } = useTranslation(); + return ( ); }; -export const StorageClassDetailsPage: React.FC = (props) => { + +export const StorageClassDetailsPage: React.FCC = (props) => { const pages = [navFactory.details(detailsPage(StorageClassDetails)), navFactory.editYaml()]; + const customActionMenu = (kindObj, obj) => { const resourceKind = referenceForModel(kindObj); const context = { [resourceKind]: obj }; @@ -200,22 +245,18 @@ export const StorageClassDetailsPage: React.FC = (props) => { ); }; - return ( - - ); + + return ; +}; + +type StorageClassListProps = { + data: StorageClassResourceKind[]; + loaded: boolean; + loadError: unknown; }; -StorageClassDetailsPage.displayName = 'StorageClassDetailsPage'; export type StorageClassDetailsProps = { obj: any; }; -export type StorageClassPageProps = { - filterLabel: string; - namespace: string; -}; +export type StorageClassPageProps = {};