Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
"@patternfly/react-catalog-view-extension": "~6.3.0",
"@patternfly/react-charts": "~8.4.0",
"@patternfly/react-code-editor": "~6.4.0",
"@patternfly/react-component-groups": "~6.4.0",
"@patternfly/react-component-groups": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-6.4.0-prerelease.15.tgz",
"@patternfly/react-core": "~6.4.0",
"@patternfly/react-data-view": "6.4.0-prerelease.12",
"@patternfly/react-drag-drop": "~6.4.0",
Expand Down Expand Up @@ -322,7 +322,8 @@
"glob-parent": "^5.1.2",
"hosted-git-info": "^3.0.8",
"lodash-es": "^4.17.23",
"postcss": "^8.2.13"
"postcss": "^8.2.13",
"@patternfly/react-component-groups": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-6.4.0-prerelease.15.tgz"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,gql,graphql}": "eslint --color --fix"
Expand Down
4 changes: 4 additions & 0 deletions frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@
"Unpin": "Unpin",
"Remove from navigation?": "Remove from navigation?",
"Remove": "Remove",
"Mark as schedulable ({{count}})": "Mark as schedulable ({{count}})",
"Mark as unschedulable ({{count}})": "Mark as unschedulable ({{count}})",
"Failed to mark {{failureCount}} of {{totalCount}} nodes as schedulable": "Failed to mark {{failureCount}} of {{totalCount}} nodes as schedulable",
"Failed to mark {{failureCount}} of {{totalCount}} nodes as unschedulable": "Failed to mark {{failureCount}} of {{totalCount}} nodes as unschedulable",
"This action cannot be undone. Deleting a node will instruct Kubernetes that the node is down or unrecoverable and delete all pods scheduled to that node. If the node is still running but unresponsive and the node is deleted, stateful workloads and persistent volumes may suffer corruption or data loss. Only delete a node that you have confirmed is completely stopped and cannot be restored.": "This action cannot be undone. Deleting a node will instruct Kubernetes that the node is down or unrecoverable and delete all pods scheduled to that node. If the node is still running but unresponsive and the node is deleted, stateful workloads and persistent volumes may suffer corruption or data loss. Only delete a node that you have confirmed is completely stopped and cannot be restored.",
"Mark as schedulable": "Mark as schedulable",
"Mark as unschedulable": "Mark as unschedulable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { FC, ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import './ConsoleDataView.scss';
import {
BulkSelect,
BulkSelectValue,
ResponsiveAction,
ResponsiveActions,
SkeletonTableBody,
Expand Down Expand Up @@ -80,6 +82,10 @@ export const ConsoleDataView = <
mock,
isResizable,
resetAllColumnWidths,
bulkSelect,
bulkActions,
selection,
actionsBreakpoint = 'lg',
}: ConsoleDataViewProps<TData, TCustomRowData, TFilters>) => {
const { t } = useTranslation();
const launchModal = useOverlay();
Expand All @@ -100,6 +106,31 @@ export const ConsoleDataView = <
matchesAdditionalFilters,
});

// Create bulkSelect component if selection is provided but bulkSelect prop is not
const defaultBulkSelect = useMemo(() => {
if (!selection?.onSelectAll || bulkSelect) return null;

const totalCount = filteredData.length;
const selectedCount = selection.selectedItems.size;

const handleBulkSelect = (value: BulkSelectValue) => {
if (value === BulkSelectValue.all || value === BulkSelectValue.page) {
selection.onSelectAll(true, filteredData);
} else if (value === BulkSelectValue.none || value === BulkSelectValue.nonePage) {
selection.onSelectAll(false, filteredData);
}
};

return (
<BulkSelect
selectedCount={selectedCount}
totalCount={totalCount}
onSelect={handleBulkSelect}
canSelectAll
/>
);
}, [selection, filteredData, bulkSelect]);

const { dataViewColumns, dataViewRows, pagination } = useConsoleDataViewData<
TData,
TCustomRowData,
Expand Down Expand Up @@ -177,6 +208,7 @@ export const ConsoleDataView = <
className={css(dataViewFilterNodes.length === 1 && 'co-console-data-view-single-filter')}
>
<DataViewToolbar
bulkSelect={bulkSelect ?? defaultBulkSelect}
filters={
dataViewFilterNodes.length > 0 && (
<DataViewFilters values={filters} onChange={(_e, values) => onSetFilters(values)}>
Expand All @@ -186,7 +218,8 @@ export const ConsoleDataView = <
}
clearAllFilters={clearAllFilters}
actions={
<ResponsiveActions breakpoint="lg">
<ResponsiveActions breakpoint={actionsBreakpoint}>
{bulkActions}
{!hideColumnManagement && (
<ResponsiveAction
isPersistent
Expand Down Expand Up @@ -247,13 +280,25 @@ export const ConsoleDataView = <
);
};

export const SELECTION_COLUMN_WIDTH = '45px';

export const cellIsStickyProps = {
isStickyColumn: true,
stickyMinWidth: '0',
};

export const nameCellProps = {
export const selectionColumnProps = {
...cellIsStickyProps,
stickyLeftOffset: '0',
};

export const nameColumnProps = {
...cellIsStickyProps,
stickyLeftOffset: SELECTION_COLUMN_WIDTH,
};

export const nameCellProps = {
...nameColumnProps,
hasRightBorder: true,
};

Expand All @@ -264,8 +309,12 @@ export const getNameCellProps = (name: string) => {
};
};

export const actionsCellProps = {
export const actionsColumnProps = {
...cellIsStickyProps,
};

export const actionsCellProps = {
...actionsColumnProps,
hasLeftBorder: true,
isActionCell: true,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { TableColumn } from '@console/dynamic-plugin-sdk/src/extensions/console-types';

const selectionColumnProps = {
isStickyColumn: true,
stickyMinWidth: '0',
stickyLeftOffset: '0',
} as const;

/**
* Creates a selection column definition for DataView tables.
* This column displays checkboxes for row selection.
*
* @example
* ```typescript
* const columns = [
* createSelectionColumn(),
* { title: 'Name', id: 'name', ... },
* ...
* ];
* ```
*/
export const createSelectionColumn = <T>(): TableColumn<T> => ({
title: '',
id: 'select',
props: selectionColumnProps,
});

type CreateSelectionCellOptions = {
/** Row index in the table */
rowIndex: number;
/** Unique ID for the item being selected */
itemId: string;
/** Whether the item is currently selected */
isSelected: boolean;
/** Callback when selection state changes */
onSelect: (itemId: string, isSelecting: boolean) => void;
/** Whether the checkbox should be disabled */
disabled?: boolean;
};

/**
* Creates a selection cell object for a DataView row.
* This cell contains the checkbox for row selection.
*
* @example
* ```typescript
* const rowCells = {
* select: createSelectionCell({
* rowIndex: 0,
* itemId: getUID(node),
* isSelected: selectedIds.has(getUID(node)),
* onSelect: onSelectItem,
* }),
* name: { cell: <NodeName node={node} /> },
* ...
* };
* ```
*/
export const createSelectionCell = ({
rowIndex,
itemId,
isSelected,
onSelect,
disabled = false,
}: CreateSelectionCellOptions) => ({
cell: '', // Checkbox is rendered via props, no content needed
props: {
...selectionColumnProps,
select: {
rowIndex,
onSelect: (_event: any, isSelecting: boolean) => {
onSelect(itemId, isSelecting);
},
isSelected,
isDisabled: disabled,
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useState, useCallback, useMemo } from 'react';

type UseDataViewSelectionOptions<T> = {
/** All data items */
data: T[];
/** Function to extract unique ID from an item */
getItemId: (item: T) => string;
/** Optional filter to exclude certain items from selection (e.g., filter out CSRs) */
filterSelectable?: (item: T) => boolean;
};

type UseDataViewSelectionResult<T> = {
/** Set of selected item IDs */
selectedIds: Set<string>;
/** Array of selected item objects */
selectedItems: T[];
/** Callback to select/deselect a single item */
onSelectItem: (itemId: string, isSelecting: boolean) => void;
/** Callback to select/deselect all filtered items */
onSelectAll: (isSelecting: boolean, filteredItems: T[]) => void;
/** Clear all selections */
clearSelection: () => void;
};

/**
* Custom hook for managing selection state in DataView components.
* Provides selection state, callbacks, and selected item objects.
*
* @example
* ```typescript
* const { selectedIds, selectedItems, onSelectItem, onSelectAll, clearSelection } =
* useDataViewSelection({
* data,
* getItemId: (node) => getUID(node),
* filterSelectable: (item) => !isCSRResource(item),
* });
* ```
*/
export const useDataViewSelection = <T>({
data,
getItemId,
filterSelectable,
}: UseDataViewSelectionOptions<T>): UseDataViewSelectionResult<T> => {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

const onSelectItem = useCallback((itemId: string, isSelecting: boolean) => {
setSelectedIds((prev) => {
const newSet = new Set(prev);
if (isSelecting) {
newSet.add(itemId);
} else {
newSet.delete(itemId);
}
return newSet;
});
}, []);

const onSelectAll = useCallback(
(isSelecting: boolean, filteredItems: T[]) => {
if (isSelecting) {
const selectableItems = filterSelectable
? filteredItems.filter(filterSelectable)
: filteredItems;
const itemIds = selectableItems.map(getItemId);
setSelectedIds(new Set(itemIds));
} else {
setSelectedIds(new Set());
}
},
[getItemId, filterSelectable],
);

const clearSelection = useCallback(() => {
setSelectedIds(new Set());
}, []);

const selectedItems = useMemo(() => {
const selectableData = filterSelectable ? data.filter(filterSelectable) : data;
return selectableData.filter((item) => selectedIds.has(getItemId(item)));
}, [data, selectedIds, getItemId, filterSelectable]);

return {
selectedIds,
selectedItems,
onSelectItem,
onSelectAll,
clearSelection,
};
};
Loading