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
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
"@patternfly/react-component-groups": "6.2.0-prerelease.10",
"@patternfly/react-console": "^6.0.0",
"@patternfly/react-core": "^6.2.2",
"@patternfly/react-data-view": "^6.2.0",
"@patternfly/react-icons": "^6.2.2",
"@patternfly/react-log-viewer": "6.3.0-prerelease.2",
"@patternfly/react-styles": "^6.2.2",
Expand Down Expand Up @@ -315,6 +316,8 @@
"node": ">=22.x"
},
"resolutions": {
"@patternfly/react-component-groups": "6.2.0-prerelease.10",
"@patternfly/react-data-view": "^6.2.0",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "5.3.x",
"hosted-git-info": "^3.0.8",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';
import { ToolbarFilter } from '@patternfly/react-core';
import * as _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src/extensions/console-types';
import AutocompleteInput from '@console/internal/components/autocomplete';

type DataViewLabelFilterProps<TData> = {
data: TData[];
title: string;
filterId: string;
onChange?: (key: string, selectedValues: string) => void;
showToolbarItem?: boolean;
};

export const DataViewLabelFilter = <TData extends K8sResourceCommon = K8sResourceCommon>({
data,
title,
filterId,
onChange,
showToolbarItem,
}: DataViewLabelFilterProps<TData>) => {
const { t } = useTranslation();

const [searchParams] = useSearchParams();
const [labelInputText, setLabelInputText] = React.useState('');
const labelSelection = searchParams.get(filterId)?.split(',') ?? [];

const applyLabelFilters = (values: string[]) => {
setLabelInputText('');
onChange?.(filterId, values.join(','));
};

return (
<ToolbarFilter
categoryName={title}
labels={labelSelection}
showToolbarItem={showToolbarItem}
deleteLabel={(_category, label: string) => {
setLabelInputText('');
applyLabelFilters(_.difference(labelSelection, [label]));
}}
deleteLabelGroup={() => {
setLabelInputText('');
applyLabelFilters([]);
}}
>
<div className="pf-v6-c-input-group co-filter-group">
<AutocompleteInput
color="purple"
onSuggestionSelect={(selected) => {
applyLabelFilters(_.uniq([...labelSelection, selected]));
}}
showSuggestions
textValue={labelInputText}
setTextValue={setLabelInputText}
placeholder={t('public~Filter by label')}
data={data}
/>
</div>
</ToolbarFilter>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import * as React from 'react';
import {
ResponsiveAction,
ResponsiveActions,
SkeletonTableBody,
} from '@patternfly/react-component-groups';
import { Bullseye, Pagination, Tooltip } from '@patternfly/react-core';
import {
DataView,
DataViewState,
DataViewTable,
DataViewTextFilter,
DataViewToolbar,
} from '@patternfly/react-data-view';
import DataViewFilters from '@patternfly/react-data-view/dist/cjs/DataViewFilters';
import { ColumnsIcon } from '@patternfly/react-icons';
import { InnerScrollContainer, Tbody, Td, Tr } from '@patternfly/react-table';
import { useTranslation } from 'react-i18next';
import {
ColumnLayout,
K8sResourceCommon,
} from '@console/dynamic-plugin-sdk/src/extensions/console-types';
import { createColumnManagementModal } from '@console/internal/components/modals';
import { TableColumn } from '@console/internal/module/k8s';
import { EmptyBox } from '@console/shared/src/components/empty-state/EmptyBox';
import { StatusBox } from '@console/shared/src/components/status/StatusBox';
import { DataViewLabelFilter } from './DataViewLabelFilter';
import { ResourceFilters, GetDataViewRows } from './types';
import { useResourceDataViewData } from './useResourceDataViewData';
import { useResourceDataViewFilters } from './useResourceDataViewFilters';

export type ResourceDataViewProps<TData, TCustomRowData, TFilters> = {
label?: string;
data: TData[];
loaded: boolean;
loadError?: any;
columns: TableColumn<TData>[];
columnLayout?: ColumnLayout;
columnManagementID?: string;
initialFilters: TFilters;
additionalFilterNodes?: React.ReactNode[];
matchesAdditionalFilters?: (resource: TData, filters: TFilters) => boolean;
getDataViewRows: GetDataViewRows<TData, TCustomRowData>;
customRowData?: TCustomRowData;
showNamespaceOverride?: boolean;
hideNameLabelFilters?: boolean;
hideLabelFilter?: boolean;
hideColumnManagement?: boolean;
mock?: boolean;
};

/**
* Console DataView component based on PatternFly DataView.
*/
export const ResourceDataView = <
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shall we add unit test for the new component?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@yapei I think we can skip adding unit tests for this component at this point in time.

The main goal of this PR was to refactor original DataView PoC code from #14897 and #15057 without putting this component into any use. This is the initial step towards using PatternFly DataView code in Console.

Any immediate issues found by @rhamilto via CronTab plugin testing should be covered in this PR. Once this PR merges, the next step should be putting this component into use on select Console data tables and resolve any other issues we find along the way.

Once we're confident about the API (props) for this component, we can add unit tests for it.

TData extends K8sResourceCommon = K8sResourceCommon,
TCustomRowData = any,
TFilters extends ResourceFilters = ResourceFilters
>({
label,
data,
loaded,
loadError,
columns,
columnLayout,
columnManagementID,
initialFilters,
additionalFilterNodes,
matchesAdditionalFilters,
getDataViewRows,
customRowData,
showNamespaceOverride,
hideNameLabelFilters,
hideLabelFilter,
hideColumnManagement,
mock,
}: ResourceDataViewProps<TData, TCustomRowData, TFilters>) => {
const { t } = useTranslation();

const { filters, onSetFilters, clearAllFilters, filteredData } = useResourceDataViewFilters<
TData,
TFilters
>({
data,
initialFilters,
matchesAdditionalFilters,
});

const { dataViewColumns, dataViewRows, pagination } = useResourceDataViewData<
TData,
TCustomRowData
>({
columns,
filteredData,
getDataViewRows,
showNamespaceOverride,
columnManagementID,
customRowData,
});

const bodyLoading = React.useMemo(
() => <SkeletonTableBody rowsCount={5} columnsCount={dataViewColumns.length} />,
[dataViewColumns.length],
);

const bodyEmpty = React.useMemo(
() => (
<Tbody>
<Tr>
<Td colSpan={dataViewColumns.length}>
<Bullseye>
{label ? t('public~No {{label}} found', { label }) : t('public~None found')}
</Bullseye>
</Td>
</Tr>
</Tbody>
),
[t, dataViewColumns.length, label],
);

const activeState = React.useMemo(() => {
if (!loaded) {
return DataViewState.loading;
}
if (filteredData.length === 0) {
return DataViewState.empty;
}
return undefined;
}, [filteredData.length, loaded]);

const dataViewFilterNodes = React.useMemo<React.ReactNode[]>(() => {
const basicFilters: React.ReactNode[] = [];

if (!hideNameLabelFilters) {
basicFilters.push(<DataViewTextFilter key="name" filterId="name" title={t('public~Name')} />);
}

if (!hideNameLabelFilters && !hideLabelFilter) {
basicFilters.push(
<DataViewLabelFilter key="label" filterId="label" title={t('public~Label')} data={data} />,
);
}

return additionalFilterNodes?.length > 0
? [...additionalFilterNodes, ...basicFilters]
: basicFilters;

// Can't use data in the deps array as it will recompute the filters and will cause the selected category to reset
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [additionalFilterNodes, t]);

return mock ? (
<EmptyBox label={label} />
) : (
<StatusBox
label={label}
data={data}
loaded={loaded}
loadError={loadError}
skeleton={<div className="loading-skeleton--table" />}
>
<DataView activeState={activeState}>
<DataViewToolbar
filters={
dataViewFilterNodes.length > 0 && (
<DataViewFilters values={filters} onChange={(_e, values) => onSetFilters(values)}>
{dataViewFilterNodes}
</DataViewFilters>
)
}
clearAllFilters={clearAllFilters}
actions={
!hideColumnManagement && (
<ResponsiveActions breakpoint="lg">
<ResponsiveAction
isPersistent
variant="plain"
onClick={() =>
createColumnManagementModal({
columnLayout,
noLimit: true,
})
}
aria-label={t('public~Column management')}
data-test="manage-columns"
>
<Tooltip content={t('public~Manage columns')} trigger="mouseenter">
<ColumnsIcon />
</Tooltip>
</ResponsiveAction>
</ResponsiveActions>
)
}
pagination={<Pagination itemCount={filteredData.length} {...pagination} />}
/>
<InnerScrollContainer>
<DataViewTable
columns={dataViewColumns}
rows={dataViewRows}
bodyStates={{ empty: bodyEmpty, loading: bodyLoading }}
gridBreakPoint=""
variant="compact"
/>
</InnerScrollContainer>
</DataView>
</StatusBox>
);
};
29 changes: 29 additions & 0 deletions frontend/packages/console-app/src/components/data-view/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DataViewTh, DataViewTd } from '@patternfly/react-data-view';
import { SortByDirection } from '@patternfly/react-table';
import {
K8sResourceCommon,
RowProps,
} from '@console/dynamic-plugin-sdk/src/extensions/console-types';

export type ResourceFilters = {
name: string;
label: string;
};

export type ResourceDataViewColumn<
TData extends K8sResourceCommon = K8sResourceCommon
> = DataViewTh & {
id: string;
title: string;
sortFunction?: string | ((filteredData: TData[], sortDirection: SortByDirection) => TData[]);
};

export type ResourceDataViewRow = DataViewTd[];

/**
* Maps Console `RowProps` data to DataView compatible format.
*/
export type GetDataViewRows<TData, TCustomRowData> = (
data: RowProps<TData, TCustomRowData>[],
columns: ResourceDataViewColumn<TData>[],
) => ResourceDataViewRow[];
Loading