Skip to content
Merged
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
6 changes: 6 additions & 0 deletions frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@
"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",
"Mark <1>{{count}}</1> nodes as unschedulable?": "Mark <1>{{count}}</1> nodes as unschedulable?",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

368: question mark inconsistent w/other action labels here. remove for consistency?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in the context of the modal, so it probably makes sense to keep it.

Screenshot 2026-05-28 at 12 52 12 PM

"Unschedulable nodes won't accept new pods. By blocking new pod assignments, you can isolate nodes to perform maintenance or decommission them without disrupting new traffic.": "Unschedulable nodes won't accept new pods. By blocking new pod assignments, you can isolate nodes to perform maintenance or decommission them without disrupting new traffic.",
"Unschedulable nodes won't accept new pods. By blocking new pod assignments, you can isolate a node to perform maintenance or decommission it without disrupting new traffic.": "Unschedulable nodes won't accept new pods. By blocking new pod assignments, you can isolate a node to perform maintenance or decommission it without disrupting new traffic.",
"Mark unschedulable": "Mark unschedulable",
"Error updating {{nodeName}}": "Error updating {{nodeName}}",
Expand Down Expand Up @@ -475,6 +477,10 @@
"Certificate approval required": "Certificate approval required",
"An error occurred. Please try again": "An error occurred. Please try again",
"No new Pods or workloads will be placed on this Node until it's marked as schedulable.": "No new Pods or workloads will be placed on this Node until it's marked as schedulable.",
"Applies to {{nodeCount}} selected nodes that are currently unschedulable.": "Applies to {{nodeCount}} selected nodes that are currently unschedulable.",
"Mark schedulable": "Mark schedulable",
"Applies to {{nodeCount}} selected nodes that are currently schedulable.": "Applies to {{nodeCount}} selected nodes that are currently schedulable.",
"Scheduling": "Scheduling",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

483: Confirming "Scheduling" intended as section heading/column header. if so, fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Button label
Screenshot 2026-05-28 at 12 55 08 PM

"Identity providers": "Identity providers",
"Mapping method": "Mapping method",
"Remove identity provider": "Remove identity provider",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import type { FC, ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useState, useEffect } from 'react';
import './ConsoleDataView.scss';
import {
ResponsiveAction,
ResponsiveActions,
SkeletonTableBody,
} from '@patternfly/react-component-groups';
import { Bullseye, Pagination, PaginationVariant, Tooltip } from '@patternfly/react-core';
import {
Banner,
Bullseye,
Button,
Pagination,
PaginationVariant,
Tooltip,
} from '@patternfly/react-core';
import {
DataView,
DataViewFilters,
Expand All @@ -17,7 +24,7 @@ import {
import { ColumnsIcon, UndoIcon } from '@patternfly/react-icons';
import { css } from '@patternfly/react-styles';
import { InnerScrollContainer, Tbody, Td, Tr } from '@patternfly/react-table';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import type {
ResourceFilters,
ConsoleDataViewProps,
Expand Down Expand Up @@ -80,6 +87,10 @@ export const ConsoleDataView = <
mock,
isResizable,
resetAllColumnWidths,
additionalActions,
customActions,
selection,
actionsBreakpoint = 'md',
}: ConsoleDataViewProps<TData, TCustomRowData, TFilters>) => {
const { t } = useTranslation();
const launchModal = useOverlay();
Expand All @@ -100,7 +111,23 @@ export const ConsoleDataView = <
matchesAdditionalFilters,
});

const { dataViewColumns, dataViewRows, pagination } = useConsoleDataViewData<
// Notify parent of filtered selected items when filters or selection changes
useEffect(() => {
if (selection?.onFilteredSelectionChange) {
const filteredSelectedItems = filteredData.filter((item) =>
selection.selectedItems.has(selection.getItemId(item)),
);
selection.onFilteredSelectionChange(filteredSelectedItems);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- the rule cannot statically verify optional chaining on `selection`; listing the full object would cause re-runs on every render
}, [
filteredData,
selection?.selectedItems,
selection?.getItemId,
selection?.onFilteredSelectionChange,
]);

const { dataViewColumns, dataViewRows, pagination, visibleItems } = useConsoleDataViewData<
TData,
TCustomRowData,
TFilters
Expand All @@ -113,6 +140,7 @@ export const ConsoleDataView = <
columnManagementID,
customRowData,
isResizable,
selection,
});

const bodyLoading = useMemo(() => <BodyLoading columns={dataViewColumns.length} />, [
Expand All @@ -136,17 +164,52 @@ export const ConsoleDataView = <

const paginationTitles = useMemo(
() => ({
paginationAriaLabel: t('public~Pagination'),
ofWord: t('public~of'),
itemsPerPage: t('public~Items per page'),
perPageSuffix: t('public~per page'),
toFirstPageAriaLabel: t('public~Go to first page'),
optionsToggleAriaLabel: t('public~Items per page'),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change from "Items per page" to "Select items per page"? active verb phrase is clearer, more actionable instruction for screen-readers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for pagination and not selection. Was missing before.

toPreviousPageAriaLabel: t('public~Go to previous page'),
toNextPageAriaLabel: t('public~Go to next page'),
toLastPageAriaLabel: t('public~Go to last page'),
}),
[t],
);

// Calculate whether to show the "select all" banner
const bannerState = useMemo(() => {
if (!selection || !loaded || filteredData.length === 0) {
return { show: false, allSelected: false };
}

const allVisibleSelected = visibleItems.every((item) =>
selection.selectedItems.has(selection.getItemId(item)),
);

const visibleCount = visibleItems.length;
const totalCount = filteredData.length;
const selectedCount = filteredData.filter((item) =>
selection.selectedItems.has(selection.getItemId(item)),
).length;

// Show banner if all visible items are selected and there are more items than visible
const shouldShow = allVisibleSelected && visibleCount > 0 && totalCount > visibleCount;
const allSelected = selectedCount === totalCount;

return { show: shouldShow, allSelected };
}, [selection, loaded, filteredData, visibleItems]);

const handleSelectAllMatching = useCallback(() => {
if (selection?.onSelectAll) {
selection.onSelectAll(true, filteredData);
}
}, [selection, filteredData]);

const handleUnselectAll = useCallback(() => {
if (selection?.onSelectAll) {
selection.onSelectAll(false, filteredData);
}
}, [selection, filteredData]);

const dataViewFilterNodes = useMemo<React.ReactNode[]>(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-label={t('public~Column management')} being removed here along w/old code block, yes? confirm that screen-reader label moved to new layout further down/elsewhere so we don't drop accessibility coverage for col management feature.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was just moved further down the file.

const basicFilters: ReactNode[] = [];

Expand Down Expand Up @@ -199,44 +262,93 @@ export const ConsoleDataView = <
}
clearAllFilters={clearAllFilters}
actions={
<ResponsiveActions breakpoint="lg">
{!hideColumnManagement && (
<ResponsiveAction
isPersistent
variant="plain"
onClick={() =>
launchModal(LazyColumnManagementModalOverlay, {
columnLayout,
noLimit: true,
})
}
aria-label={t('public~Column management')}
data-test="manage-columns"
>
<Tooltip content={t('public~Manage columns')} trigger="mouseenter">
<ColumnsIcon />
</Tooltip>
</ResponsiveAction>
)}
{isResizable && resetAllColumnWidths && (
<ResponsiveAction
isPersistent
variant="plain"
onClick={handleResetColumnWidths}
aria-label={t('public~Reset column widths')}
data-test="reset-column-widths"
>
<Tooltip content={t('public~Reset column widths')} trigger="mouseenter">
<UndoIcon />
</Tooltip>
</ResponsiveAction>
)}
</ResponsiveActions>
<>
<ResponsiveActions breakpoint={actionsBreakpoint}>
{!hideColumnManagement && (
<ResponsiveAction
isPersistent
variant="plain"
onClick={() =>
launchModal(LazyColumnManagementModalOverlay, {
columnLayout,
noLimit: true,
})
}
aria-label={t('public~Column management')}
data-test="manage-columns"
>
<Tooltip content={t('public~Manage columns')} trigger="mouseenter">
<ColumnsIcon />
</Tooltip>
</ResponsiveAction>
)}
{isResizable && resetAllColumnWidths && (
<ResponsiveAction
isPersistent
variant="plain"
onClick={handleResetColumnWidths}
aria-label={t('public~Reset column widths')}
data-test="reset-column-widths"
>
<Tooltip content={t('public~Reset column widths')} trigger="mouseenter">
<UndoIcon />
</Tooltip>
</ResponsiveAction>
)}
{additionalActions}
</ResponsiveActions>
{customActions}
</>
}
pagination={
<Pagination itemCount={filteredData.length} titles={paginationTitles} {...pagination} />
<Pagination
itemCount={filteredData.length}
titles={paginationTitles}
variant={PaginationVariant.top}
isCompact
{...pagination}
/>
}
/>
{bannerState.show && (
<Banner
className="pf-v6-u-mb-md"
screenReaderText={
bannerState.allSelected
? t('public~You selected all {{count}} {{label}}.', {
count: filteredData.length,
label: label || t('public~items'),
})
: t('public~You selected all {{label}} on this page.', {
label: label || t('public~items'),
})
}
>
{bannerState.allSelected ? (
<>
<Trans ns="public" i18nKey="You selected all <1>{{count}}</1> {{label}}.">
You selected all <strong>{{ count: filteredData.length }}</strong>{' '}
{{ label: label || t('public~items') }}.
</Trans>{' '}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

330-333 AND 342-346: update translation strings to support conditional pluralization keys instead of hard-coding plural structure? as you know, if selection count equals 1, UI will display grammatically incorrect strings like "All 1 matching items are selected."

<Button variant="link" isInline onClick={handleUnselectAll}>
{t('public~Clear all.')}
</Button>
</>
) : (
<>
<Trans ns="public" i18nKey="You selected all {{label}} on this page.">
You selected all {{ label: label || t('public~items') }} on this page.
</Trans>{' '}
<Button variant="link" isInline onClick={handleSelectAllMatching}>
<Trans ns="public" i18nKey="Select all <1>{{count}}</1> {{label}}.">
Select all <strong>{{ count: filteredData.length }}</strong>{' '}
{{ label: label || t('public~items') }}.
</Trans>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

347-351: add period to end of text string inside i18nKey so it reads "Select all <1>{{count}}</1> matching {{label}}." + make sure it handles singular/plural states.

</Button>
</>
)}
</Banner>
)}
<InnerScrollContainer>
<DataViewTable
key={tableKey}
Expand All @@ -255,29 +367,53 @@ export const ConsoleDataView = <
itemCount={filteredData.length}
titles={paginationTitles}
variant={PaginationVariant.bottom}
isCompact
{...pagination}
/>
</DataView>
</StatusBox>
);
};

export const SELECTION_COLUMN_WIDTH = '45px';

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

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

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

export const getNameCellProps = (name: string) => {
return {
...nameCellProps,
'data-test': `data-view-cell-${name}-name`,
};
};
/**
* Returns name column props with appropriate offset based on whether bulk select is enabled.
* Use this for column definitions.
* @param hasRightBorder - Whether to include hasRightBorder (default: true)
* @param withBulkSelect - Whether the table has bulk selection enabled (default: false)
*/
export const getNameColumnProps = (hasRightBorder = true, withBulkSelect = false) => ({
...cellIsStickyProps,
...(hasRightBorder && { hasRightBorder: true }),
...(withBulkSelect && { stickyLeftOffset: SELECTION_COLUMN_WIDTH }),
});

/**
* Returns name cell props with appropriate offset based on whether bulk select is enabled.
* Use this for row cell definitions.
* @param name - The name to use in the data-test attribute
* @param withBulkSelect - Whether the table has bulk selection enabled (default: false)
*/
export const getNameCellProps = (name: string, withBulkSelect = false) => ({
...getNameColumnProps(true, withBulkSelect),
'data-test': `data-view-cell-${name}-name`,
});

export const actionsCellProps = {
...cellIsStickyProps,
Expand Down
Loading