diff --git a/frontend/public/components/cron-job.tsx b/frontend/public/components/cron-job.tsx index cb5e0084090..5f52f40e8e7 100644 --- a/frontend/public/components/cron-job.tsx +++ b/frontend/public/components/cron-job.tsx @@ -39,7 +39,7 @@ import { } from './utils'; import { ResourceEventStream } from './events'; import { CronJobModel } from '../models'; -import { PodList, filters as podFilters } from './pod'; +import { PodList, getFilters as getPodFilters } from './pod'; import { JobsList } from './job'; const { common } = Kebab.factory; @@ -183,37 +183,41 @@ const getPodsWatcher = (namespace: string) => { ]; }; -export const CronJobPodsComponent: React.FC = ({ obj }) => ( -
- - , - ) => { - if (!_resources.jobs.loaded || !_resources.pods.loaded) { - return []; - } - const jobs = _resources.jobs.data.filter((job) => - job.metadata?.ownerReferences?.find((ref) => ref.uid === obj.metadata.uid), - ); - return ( - jobs && - jobs.reduce((acc, job) => { - acc.push(...getPodsForResource(job, _resources)); - return acc; - }, []) - ); - }} - kinds={['Pods']} - ListComponent={PodList} - rowFilters={podFilters} - /> - -
-); +export const CronJobPodsComponent: React.FC = ({ obj }) => { + const { t } = useTranslation(); + const podFilters = React.useMemo(() => getPodFilters(t), [t]); + return ( +
+ + , + ) => { + if (!_resources.jobs.loaded || !_resources.pods.loaded) { + return []; + } + const jobs = _resources.jobs.data.filter((job) => + job.metadata?.ownerReferences?.find((ref) => ref.uid === obj.metadata.uid), + ); + return ( + jobs && + jobs.reduce((acc, job) => { + acc.push(...getPodsForResource(job, _resources)); + return acc; + }, []) + ); + }} + kinds={['Pods']} + ListComponent={PodList} + rowFilters={podFilters} + /> + +
+ ); +}; export type CronJobJobsComponentProps = { obj: K8sResourceKind; diff --git a/frontend/public/components/factory/Table/VirtualizedTable.tsx b/frontend/public/components/factory/Table/VirtualizedTable.tsx new file mode 100644 index 00000000000..0df94ba3986 --- /dev/null +++ b/frontend/public/components/factory/Table/VirtualizedTable.tsx @@ -0,0 +1,294 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { + Table as PfTable, + TableHeader, + TableGridBreakpoint, + OnSelect, + SortByDirection, + ICell, +} from '@patternfly/react-table'; +import { AutoSizer, WindowScroller } from '@patternfly/react-virtualized-extension'; +import { ALL_NAMESPACES_KEY, useActiveNamespace } from '@console/shared'; + +import VirtualizedTableBody from './VirtualizedTableBody'; +import { history, StatusBox, WithScrollContainer } from '../../utils'; +import { sortResourceByValue } from './sort'; + +const BREAKPOINT_SM = 576; +const BREAKPOINT_MD = 768; +const BREAKPOINT_LG = 992; +const BREAKPOINT_XL = 1200; +const BREAKPOINT_XXL = 1400; +const MAX_COL_XS = 2; +const MAX_COL_SM = 4; +const MAX_COL_MD = 4; +const MAX_COL_LG = 6; +const MAX_COL_XL = 8; + +const isColumnVisible = ( + widthInPixels: number, + columnID: string, + columns: Set = new Set(), + showNamespaceOverride: boolean, + namespace: string, +) => { + const showNamespace = columnID !== 'namespace' || !namespace || showNamespaceOverride; + if (_.isEmpty(columns) && showNamespace) { + return true; + } + if (!columns.has(columnID) || !showNamespace) { + return false; + } + const columnIndex = [...columns].indexOf(columnID); + if (widthInPixels < BREAKPOINT_SM) { + return columnIndex < MAX_COL_XS; + } + if (widthInPixels < BREAKPOINT_MD) { + return columnIndex < MAX_COL_SM; + } + if (widthInPixels < BREAKPOINT_LG) { + return columnIndex < MAX_COL_MD; + } + if (widthInPixels < BREAKPOINT_XL) { + return columnIndex < MAX_COL_LG; + } + if (widthInPixels < BREAKPOINT_XXL) { + return columnIndex < MAX_COL_XL; + } + return true; +}; + +const getActiveColumns = ( + windowWidth: number, + allColumns: TableColumn[], + activeColumns: Set, + columnManagementID: string, + showNamespaceOverride: boolean, + namespace: string, +) => { + let columns = [...allColumns]; + if (_.isEmpty(activeColumns)) { + activeColumns = new Set( + columns.map((col) => { + if (col.id && !col.additional) { + return col.id; + } + }), + ); + } + if (columnManagementID) { + columns = columns?.filter( + (col) => + isColumnVisible(windowWidth, col.id, activeColumns, showNamespaceOverride, namespace) || + col.title === '', + ); + } else { + columns = columns?.filter((col) => activeColumns.has(col.id) || col.title === ''); + } + + const showNamespace = !namespace || showNamespaceOverride; + if (!showNamespace) { + columns = columns.filter((column) => column.id !== 'namespace'); + } + return columns; +}; + +export type TableColumn = ICell & { + title: string; + id?: string; + additional?: boolean; + sort?: ((data: D[], sortDirection: SortByDirection) => D[]) | string; +}; + +export type RowProps = { + obj: D; + rowData: R; +}; + +type VirtualizedTableProps = { + data: D[]; + unfilteredData: D[]; + loaded: boolean; + loadError: any; + columns: TableColumn[]; + Row: React.ComponentType>; + NoDataEmptyMsg?: React.ComponentType<{}>; + EmptyMsg?: React.ComponentType<{}>; + scrollNode?: () => HTMLElement; + onSelect?: OnSelect; + label?: string; + 'aria-label'?: string; + gridBreakPoint?: TableGridBreakpoint; + activeColumns?: Set; + columnManagementID?: string; + showNamespaceOverride?: boolean; + rowData?: R; +}; + +const VirtualizedTable = ({ + data, + loaded, + loadError, + columns: allColumns, + NoDataEmptyMsg, + EmptyMsg, + scrollNode, + label, + 'aria-label': ariaLabel, + gridBreakPoint = TableGridBreakpoint.none, + onSelect, + Row, + activeColumns, + columnManagementID, + showNamespaceOverride, + rowData, + unfilteredData, +}: VirtualizedTableProps) => { + const columnShift = onSelect ? 1 : 0; //shift indexes by 1 if select provided + const [sortBy, setSortBy] = React.useState<{ + index: number; + direction: SortByDirection; + }>({ index: columnShift, direction: SortByDirection.asc }); + + const [windowWidth, setWindowWidth] = React.useState(window.innerWidth); + const [namespace] = useActiveNamespace(); + + const columns = React.useMemo( + () => + getActiveColumns( + windowWidth, + allColumns, + activeColumns, + columnManagementID, + showNamespaceOverride, + namespace === ALL_NAMESPACES_KEY ? undefined : namespace, + ), + [windowWidth, allColumns, activeColumns, columnManagementID, showNamespaceOverride, namespace], + ); + + const applySort = React.useCallback( + (index, direction) => { + const url = new URL(window.location.href); + const sp = new URLSearchParams(window.location.search); + + const sortColumn = columns[index - columnShift]; + if (sortColumn) { + sp.set('orderBy', direction); + sp.set('sortBy', sortColumn.title); + history.replace(`${url.pathname}?${sp.toString()}${url.hash}`); + setSortBy({ + index, + direction, + }); + } + }, + [columnShift, columns], + ); + + data = React.useMemo(() => { + const sortColumn = columns[sortBy.index - columnShift]; + if (!sortColumn.sort) { + return data; + } else if (typeof sortColumn.sort === 'string') { + return data.sort( + sortResourceByValue(sortBy.direction, (obj) => _.get(obj, sortColumn.sort as string, '')), + ); + } + return sortColumn.sort(data, sortBy.direction); + }, [columnShift, columns, data, sortBy.direction, sortBy.index]); + + React.useEffect(() => { + const handleResize = _.debounce(() => setWindowWidth(window.innerWidth), 100); + + const sp = new URLSearchParams(window.location.search); + const columnIndex = _.findIndex(columns, { title: sp.get('sortBy') }); + + if (columnIndex > -1) { + const sortOrder = + sp.get('orderBy') === SortByDirection.desc.valueOf() + ? SortByDirection.desc + : SortByDirection.asc; + setSortBy({ + index: columnIndex + columnShift, + direction: sortOrder, + }); + } + + // re-render after resize + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onSort = React.useCallback( + (event, index, direction) => { + event.preventDefault(); + applySort(index, direction); + }, + [applySort], + ); + + const renderVirtualizedTable = (scrollContainer) => ( + + {({ height, isScrolling, registerChild, onChildScroll, scrollTop }) => ( + + {({ width }) => ( +
+ + Row={Row} + height={height} + isScrolling={isScrolling} + onChildScroll={onChildScroll} + data={data} + columns={columns} + scrollTop={scrollTop} + width={width} + rowData={rowData} + /> +
+ )} +
+ )} +
+ ); + + return ( +
+ } + data={data} + loaded={loaded} + loadError={loadError} + unfilteredData={unfilteredData} + label={label} + NoDataEmptyMsg={NoDataEmptyMsg} + EmptyMsg={EmptyMsg} + > +
+ + + + {scrollNode ? ( + renderVirtualizedTable(scrollNode) + ) : ( + {renderVirtualizedTable} + )} +
+
+
+ ); +}; + +export default VirtualizedTable; diff --git a/frontend/public/components/factory/Table/VirtualizedTableBody.tsx b/frontend/public/components/factory/Table/VirtualizedTableBody.tsx new file mode 100644 index 00000000000..09c400d8aa8 --- /dev/null +++ b/frontend/public/components/factory/Table/VirtualizedTableBody.tsx @@ -0,0 +1,100 @@ +import * as React from 'react'; +import { VirtualTableBody } from '@patternfly/react-virtualized-extension'; +import { CellMeasurerCache, CellMeasurer } from 'react-virtualized'; +import { Scroll } from '@patternfly/react-virtualized-extension/dist/js/components/Virtualized/types'; +import { K8sResourceCommon } from '@console/dynamic-plugin-sdk'; +import { TableColumn, RowProps } from './VirtualizedTable'; +import { TableRow } from '../table'; + +type VirtualizedTableBodyProps = { + Row: React.ComponentType>; + data: D[]; + height: number; + isScrolling: boolean; + onChildScroll: (params: Scroll) => void; + columns: TableColumn[]; + scrollTop: number; + width: number; + rowData?: R; + getRowId?: (obj: D) => string; + getRowTitle?: (obj: D) => string; + getRowClassName?: (obj: D) => string; +}; + +const RowMemo = React.memo & { Row: React.ComponentType> }>( + ({ Row, ...props }) => , +); + +const VirtualizedTableBody = ({ + Row, + height, + isScrolling, + onChildScroll, + data, + columns, + scrollTop, + width, + rowData, + getRowId, + getRowTitle, + getRowClassName, +}: VirtualizedTableBodyProps) => { + const cellMeasurementCache = new CellMeasurerCache({ + fixedWidth: true, + minHeight: 44, + keyMapper: (rowIndex) => (data?.[rowIndex] as K8sResourceCommon)?.metadata?.uid || rowIndex, // TODO custom keyMapper ? + }); + + const rowRenderer = ({ index, isVisible, key, style, parent }) => { + const rowArgs: RowProps = { + obj: data[index], + rowData, + }; + + // do not render non visible elements (this excludes overscan) + if (!isVisible) { + return null; + } + return ( + + + + + + ); + }; + + return ( + + ); +}; + +export default VirtualizedTableBody; diff --git a/frontend/public/components/factory/Table/sort.ts b/frontend/public/components/factory/Table/sort.ts new file mode 100644 index 00000000000..223e5caaa39 --- /dev/null +++ b/frontend/public/components/factory/Table/sort.ts @@ -0,0 +1,28 @@ +import { SortByDirection } from '@patternfly/react-table'; + +import { K8sResourceCommon } from '../../../module/k8s'; + +const isNumber = (value): value is number => Number.isFinite(value); + +export const sortResourceByValue = ( + sortDirection: SortByDirection, + valueGetter: (obj: D) => V, +) => (a: D, b: D): number => { + const lang = navigator.languages[0] || navigator.language; + // Use `localCompare` with `numeric: true` for a natural sort order (e.g., pv-1, pv-9, pv-10) + const compareOpts = { numeric: true, ignorePunctuation: true }; + const aValue = valueGetter(a); + const bValue = valueGetter(b); + const result: number = + isNumber(aValue) && isNumber(bValue) + ? aValue - bValue + : `${aValue}`.localeCompare(`${bValue}`, lang, compareOpts); + if (result !== 0) { + return sortDirection === SortByDirection.asc ? result : result * -1; + } + + // Use name as a secondary sort for a stable sort. + const aName = (a as K8sResourceCommon)?.metadata?.name || ''; + const bName = (b as K8sResourceCommon)?.metadata?.name || ''; + return aName.localeCompare(bName, lang, compareOpts); +}; diff --git a/frontend/public/components/machine.tsx b/frontend/public/components/machine.tsx index b325c18c8fe..57e489915e2 100644 --- a/frontend/public/components/machine.tsx +++ b/frontend/public/components/machine.tsx @@ -16,7 +16,7 @@ import { MachineModel } from '../models'; import { MachineKind, referenceForModel, Selector } from '../module/k8s'; import { Conditions } from './conditions'; import NodeIPList from '@console/app/src/components/nodes/NodeIPList'; -import { DetailsPage, Table, TableData, RowFunctionArgs } from './factory'; +import { DetailsPage, TableData } from './factory'; import ListPageFilter from './factory/ListPage/ListPageFilter'; import ListPageHeader from './factory/ListPage/ListPageHeader'; import ListPageBody from './factory/ListPage/ListPageBody'; @@ -34,6 +34,9 @@ import { } from './utils'; import { ResourceEventStream } from './events'; import { useK8sWatchResource } from './utils/k8s-watch-hook'; +import VirtualizedTable, { RowProps, TableColumn } from './factory/Table/VirtualizedTable'; +import { sortResourceByValue } from './factory/Table/sort'; + const { common } = Kebab.factory; const menuActions = [...Kebab.getExtensionsActionsForKind(MachineModel), ...common]; export const machineReference = referenceForModel(MachineModel); @@ -52,7 +55,7 @@ const tableColumnClasses = [ const getMachineProviderState = (obj: MachineKind): string => obj?.status?.providerStatus?.instanceState; -const MachineTableRow: React.FC> = ({ obj }) => { +const MachineTableRow: React.FC> = ({ obj }) => { const nodeName = getMachineNodeName(obj); const region = getMachineRegion(obj); const zone = getMachineZone(obj); @@ -168,54 +171,56 @@ const MachineDetails: React.SFC = ({ obj }: { obj: MachineK type MachineListProps = { data: MachineKind[]; + unfilteredData: MachineKind[]; loaded: boolean; loadError: any; }; export const MachineList: React.FC = (props) => { const { t } = useTranslation(); - const MachineTableHeader = () => { - return [ + + const machineTableColumn = React.useMemo[]>( + () => [ { title: t('public~Name'), - sortField: 'metadata.name', + sort: 'metadata.name', transforms: [sortable], props: { className: tableColumnClasses[0] }, }, { title: t('public~Namespace'), - sortField: 'metadata.namespace', + sort: 'metadata.namespace', transforms: [sortable], props: { className: tableColumnClasses[1] }, id: 'namespace', }, { title: t('public~Node'), - sortField: 'status.nodeRef.name', + sort: 'status.nodeRef.name', transforms: [sortable], props: { className: tableColumnClasses[2] }, }, { title: t('public~Phase'), - sortFunc: 'machinePhase', + sort: (data, direction) => data.sort(sortResourceByValue(direction, getMachinePhase)), transforms: [sortable], props: { className: tableColumnClasses[3] }, }, { title: t('public~Provider state'), - sortField: 'status.providerStatus.instanceState', + sort: 'status.providerStatus.instanceState', transforms: [sortable], props: { className: tableColumnClasses[4] }, }, { title: t('public~Region'), - sortField: "metadata.labels['machine.openshift.io/region']", + sort: "metadata.labels['machine.openshift.io/region']", transforms: [sortable], props: { className: tableColumnClasses[5] }, }, { title: t('public~Availability zone'), - sortField: "metadata.labels['machine.openshift.io/zone']", + sort: "metadata.labels['machine.openshift.io/zone']", transforms: [sortable], props: { className: tableColumnClasses[6] }, }, @@ -223,15 +228,16 @@ export const MachineList: React.FC = (props) => { title: '', props: { className: tableColumnClasses[7] }, }, - ]; - }; + ], + [t], + ); + return ( - {...props} aria-label={t('public~Machines')} - Header={MachineTableHeader} + columns={machineTableColumn} Row={MachineTableRow} - virtualize /> ); }; @@ -271,7 +277,12 @@ export const MachinePage: React.FC = ({ hideLabelFilter={hideLabelFilter} hideColumnManagement={hideColumnManagement} /> - + ); diff --git a/frontend/public/components/pod.tsx b/frontend/public/components/pod.tsx index ca54e43ade3..cc50d88497b 100644 --- a/frontend/public/components/pod.tsx +++ b/frontend/public/components/pod.tsx @@ -2,16 +2,17 @@ import * as React from 'react'; // FIXME upgrading redux types is causing many errors at this time // eslint-disable-next-line @typescript-eslint/ban-ts-ignore // @ts-ignore -import { useSelector, useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import { sortable } from '@patternfly/react-table'; import { useTranslation } from 'react-i18next'; -import i18next from 'i18next'; +import { TFunction } from 'i18next'; import * as classNames from 'classnames'; import * as _ from 'lodash-es'; import { Button, Popover, Grid, GridItem } from '@patternfly/react-core'; import { Status, + TableColumnsType, LazyActionMenu, ActionServiceProvider, ActionMenu, @@ -42,7 +43,7 @@ import { } from '../module/k8s/pods'; import { getContainerState, getContainerStatus } from '../module/k8s/container'; import { ResourceEventStream } from './events'; -import { DetailsPage, Table, TableData, RowFunctionArgs } from './factory'; +import { DetailsPage, TableData } from './factory'; import ListPageBody from './factory/ListPage/ListPageBody'; import ListPageHeader from './factory/ListPage/ListPageHeader'; import ListPageFilter from './factory/ListPage/ListPageFilter'; @@ -98,6 +99,8 @@ import DashboardCardBody from '@console/shared/src/components/dashboard/dashboar import { useK8sWatchResource } from './utils/k8s-watch-hook'; import { useListPageFilter } from './factory/ListPage/filter-hook'; import { RowFilter } from './filter-toolbar'; +import VirtualizedTable, { RowProps, TableColumn } from './factory/Table/VirtualizedTable'; +import { sortResourceByValue } from './factory/Table/sort'; // Only request metrics if the device's screen width is larger than the // breakpoint where metrics are visible. @@ -219,109 +222,110 @@ const podColumnInfo = Object.freeze({ const kind = 'Pod'; const columnManagementID = referenceForModel(PodModel); -const getHeader = (showNodes) => { - return () => { - return [ - { - title: i18next.t(podColumnInfo.name.title), - id: podColumnInfo.name.id, - sortField: 'metadata.name', - transforms: [sortable], - props: { className: podColumnInfo.name.classes }, - }, - { - title: i18next.t(podColumnInfo.namespace.title), - id: podColumnInfo.namespace.id, - sortField: 'metadata.namespace', - transforms: [sortable], - props: { className: podColumnInfo.namespace.classes }, - }, - { - title: i18next.t(podColumnInfo.status.title), - id: podColumnInfo.status.id, - sortFunc: 'podPhase', - transforms: [sortable], - props: { className: podColumnInfo.status.classes }, - }, - { - title: i18next.t(podColumnInfo.ready.title), - id: podColumnInfo.ready.id, - sortFunc: 'podReadiness', - transforms: [sortable], - props: { className: podColumnInfo.ready.classes }, - }, - { - title: i18next.t(podColumnInfo.restarts.title), - id: podColumnInfo.restarts.id, - sortFunc: 'podRestarts', - transforms: [sortable], - props: { className: podColumnInfo.restarts.classes }, - }, - { - title: showNodes - ? i18next.t(podColumnInfo.node.title) - : i18next.t(podColumnInfo.owner.title), - id: podColumnInfo.owner.id, - sortField: showNodes ? 'spec.nodeName' : 'metadata.ownerReferences[0].name', - transforms: [sortable], - props: { className: podColumnInfo.owner.classes }, - }, - { - title: i18next.t(podColumnInfo.memory.title), - id: podColumnInfo.memory.id, - sortFunc: 'podMemory', - transforms: [sortable], - props: { className: podColumnInfo.memory.classes }, - }, - { - title: i18next.t(podColumnInfo.cpu.title), - id: podColumnInfo.cpu.id, - sortFunc: 'podCPU', - transforms: [sortable], - props: { className: podColumnInfo.cpu.classes }, - }, - { - title: i18next.t(podColumnInfo.created.title), - id: podColumnInfo.created.id, - sortField: 'metadata.creationTimestamp', - transforms: [sortable], - props: { className: podColumnInfo.created.classes }, - }, - { - title: i18next.t(podColumnInfo.node.title), - id: podColumnInfo.node.id, - sortField: 'spec.nodeName', - transforms: [sortable], - props: { className: podColumnInfo.node.classes }, - additional: true, - }, - { - title: i18next.t(podColumnInfo.labels.title), - id: podColumnInfo.labels.id, - sortField: 'metadata.labels', - transforms: [sortable], - props: { className: podColumnInfo.labels.classes }, - additional: true, - }, - { - title: i18next.t(podColumnInfo.ipaddress.title), - id: podColumnInfo.ipaddress.id, - sortField: 'status.podIP', - transforms: [sortable], - props: { className: podColumnInfo.ipaddress.classes }, - additional: true, - }, - { - title: '', - props: { className: Kebab.columnClass }, - }, - ]; - }; -}; +const getColumns = (showNodes: boolean, t: TFunction): TableColumn[] => [ + { + title: t(podColumnInfo.name.title), + id: podColumnInfo.name.id, + sort: 'metadata.name', + transforms: [sortable], + props: { className: podColumnInfo.name.classes }, + }, + { + title: t(podColumnInfo.namespace.title), + id: podColumnInfo.namespace.id, + sort: 'metadata.namespace', + transforms: [sortable], + props: { className: podColumnInfo.namespace.classes }, + }, + { + title: t(podColumnInfo.status.title), + id: podColumnInfo.status.id, + sort: (data, direction) => data.sort(sortResourceByValue(direction, podPhase)), + transforms: [sortable], + props: { className: podColumnInfo.status.classes }, + }, + { + title: t(podColumnInfo.ready.title), + id: podColumnInfo.ready.id, + sort: (data, direction) => + data.sort(sortResourceByValue(direction, (obj) => podReadiness(obj).readyCount)), + transforms: [sortable], + props: { className: podColumnInfo.ready.classes }, + }, + { + title: t(podColumnInfo.restarts.title), + id: podColumnInfo.restarts.id, + sort: (data, direction) => data.sort(sortResourceByValue(direction, podRestarts)), + transforms: [sortable], + props: { className: podColumnInfo.restarts.classes }, + }, + { + title: showNodes ? t(podColumnInfo.node.title) : t(podColumnInfo.owner.title), + id: podColumnInfo.owner.id, + sort: showNodes ? 'spec.nodeName' : 'metadata.ownerReferences[0].name', + transforms: [sortable], + props: { className: podColumnInfo.owner.classes }, + }, + { + title: t(podColumnInfo.memory.title), + id: podColumnInfo.memory.id, + sort: (data, direction) => + data.sort( + sortResourceByValue(direction, (obj) => UIActions.getPodMetric(obj, 'memory')), + ), + transforms: [sortable], + props: { className: podColumnInfo.memory.classes }, + }, + { + title: t(podColumnInfo.cpu.title), + id: podColumnInfo.cpu.id, + sort: (data, direction) => + data.sort( + sortResourceByValue(direction, (obj) => UIActions.getPodMetric(obj, 'cpu')), + ), + transforms: [sortable], + props: { className: podColumnInfo.cpu.classes }, + }, + { + title: t(podColumnInfo.created.title), + id: podColumnInfo.created.id, + sort: 'metadata.creationTimestamp', + transforms: [sortable], + props: { className: podColumnInfo.created.classes }, + }, + { + title: t(podColumnInfo.node.title), + id: podColumnInfo.node.id, + sort: 'spec.nodeName', + transforms: [sortable], + props: { className: podColumnInfo.node.classes }, + additional: true, + }, + { + title: t(podColumnInfo.labels.title), + id: podColumnInfo.labels.id, + sort: 'metadata.labels', + transforms: [sortable], + props: { className: podColumnInfo.labels.classes }, + additional: true, + }, + { + title: t(podColumnInfo.ipaddress.title), + id: podColumnInfo.ipaddress.id, + sort: 'status.podIP', + transforms: [sortable], + props: { className: podColumnInfo.ipaddress.classes }, + additional: true, + }, + { + title: '', + props: { className: Kebab.columnClass }, + }, +]; -const getSelectedColumns = (showNodes: boolean) => { +const getSelectedColumns = (showNodes: boolean, t: TFunction) => { return new Set( - getHeader(showNodes)().reduce((acc, column) => { + getColumns(showNodes, t).reduce((acc, column) => { if (column.id && !column.additional) { acc.push(column.id); } @@ -330,10 +334,11 @@ const getSelectedColumns = (showNodes: boolean) => { ); }; -const PodTableRow: React.FC> = ({ +const PodTableRow: React.FC> = ({ obj: pod, - customData: { showNodes, showNamespaceOverride, tableColumns }, + rowData: { showNodes, showNamespaceOverride, tableColumns }, }) => { + const { t } = useTranslation(); const { name, namespace, creationTimestamp, labels } = pod.metadata; const bytes: number = useSelector(({ UI }) => { const metrics = UI.getIn(['metrics', 'pod']); @@ -347,8 +352,7 @@ const PodTableRow: React.FC> = ({ const phase = podPhase(pod); const restarts = podRestarts(pod); const columns: Set = - tableColumns?.length > 0 ? new Set(tableColumns) : getSelectedColumns(showNodes); - const { t } = useTranslation(); + tableColumns?.length > 0 ? new Set(tableColumns) : getSelectedColumns(showNodes, t); const resourceKind = referenceFor(pod); const context = { [resourceKind]: pod }; return ( @@ -843,36 +847,35 @@ export const PodList: React.FC = ({ showNamespaceOverride, showNod undefined, true, ); - const selectedColumns: Set = - tableColumns?.[columnManagementID]?.length > 0 - ? new Set(tableColumns[columnManagementID]) - : null; - - const customData = React.useMemo( - () => ({ tableColumns: tableColumns?.[columnManagementID], showNodes, showNamespaceOverride }), - [showNamespaceOverride, showNodes, tableColumns], - ); + const rowData = React.useMemo(() => { + const selectedColumns: Set = + tableColumns?.[columnManagementID]?.length > 0 + ? new Set(tableColumns[columnManagementID]) + : null; + return { + showNamespaceOverride, + showNodes, + tableColumns: selectedColumns ? [...selectedColumns] : null, + }; + }, [tableColumns, showNamespaceOverride, showNodes]); + const columns = React.useMemo(() => getColumns(showNodes, t), [showNodes, t]); return ( userSettingsLoaded && ( -
{...props} - activeColumns={selectedColumns} - columnManagementID={columnManagementID} - showNamespaceOverride={showNamespaceOverride} aria-label={t('public~Pods')} - Header={getHeader(showNodes)} + columns={columns} Row={PodTableRow} - customData={customData} - virtualize + rowData={rowData} /> ) ); }; PodList.displayName = 'PodList'; -export const filters: RowFilter[] = [ +export const getFilters = (t: TFunction): RowFilter[] => [ { - filterGroupName: i18next.t('public~Status'), + filterGroupName: t('public~Status'), type: 'pod-status', reducer: podPhaseFilterReducer, items: [ @@ -899,11 +902,12 @@ export const PodsPage: React.FC = ({ hideNameLabelFilters, hideLabelFilter, hideColumnManagement, + nameFilter, showNamespaceOverride, }) => { const { t } = useTranslation(); const dispatch = useDispatch(); - const [tableColumns, , userSettingsLoaded] = useUserSettingsCompatibility( + const [tableColumns, , userSettingsLoaded] = useUserSettingsCompatibility( COLUMN_MANAGEMENT_CONFIGMAP_KEY, COLUMN_MANAGEMENT_LOCAL_STORAGE_KEY, undefined, @@ -940,7 +944,11 @@ export const PodsPage: React.FC = ({ fieldSelector, }); - const [data, filteredData, onFilterChange] = useListPageFilter(pods); + const [data, filteredData, onFilterChange] = useListPageFilter(pods, undefined, { + name: { selected: [nameFilter] }, + }); + + const filters = React.useMemo(() => getFilters(t), [t]); return ( userSettingsLoaded && ( @@ -959,7 +967,7 @@ export const PodsPage: React.FC = ({ rowFilters={filters} onFilterChange={onFilterChange} columnLayout={{ - columns: getHeader(showNodes)().map((column) => + columns: getColumns(showNodes, t).map((column) => _.pick(column, ['title', 'additional', 'id']), ), id: columnManagementID, @@ -976,8 +984,10 @@ export const PodsPage: React.FC = ({ /> @@ -1029,7 +1039,7 @@ type PodDetailsProps = { obj: PodKind; }; -type RowCustomData = { +type PodRowData = { tableColumns: string[]; showNodes?: boolean; showNamespaceOverride?: boolean; @@ -1037,6 +1047,7 @@ type RowCustomData = { type PodListProps = { data: PodKind[]; + unfilteredData: PodKind[]; loaded: boolean; loadError: any; showNodes?: boolean; @@ -1053,6 +1064,7 @@ type PodPageProps = { hideLabelFilter?: boolean; hideNameLabelFilters?: boolean; hideColumnManagement?: boolean; + nameFilter?: string; showNamespaceOverride?: boolean; };