Skip to content

Commit a652a44

Browse files
committed
Add ResourceDataView component and related code
1 parent 4dd9a7c commit a652a44

File tree

12 files changed

+578
-23
lines changed

12 files changed

+578
-23
lines changed

frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
"@patternfly/react-component-groups": "6.2.0-prerelease.10",
144144
"@patternfly/react-console": "^6.0.0",
145145
"@patternfly/react-core": "^6.2.2",
146+
"@patternfly/react-data-view": "^6.2.0",
146147
"@patternfly/react-icons": "^6.2.2",
147148
"@patternfly/react-log-viewer": "^6.1.0",
148149
"@patternfly/react-styles": "^6.2.2",
@@ -324,6 +325,8 @@
324325
"ua-parser-js": "^0.7.24",
325326
"glob-parent": "^5.1.2",
326327
"postcss": "^8.2.13",
328+
"@patternfly/react-component-groups": "6.2.0-prerelease.10",
329+
"@patternfly/react-data-view": "^6.2.0",
327330
"async": "^3.2.5"
328331
},
329332
"lint-staged": {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as React from 'react';
2+
import { ToolbarFilter } from '@patternfly/react-core';
3+
import * as _ from 'lodash';
4+
import { useTranslation } from 'react-i18next';
5+
import { useSearchParams } from 'react-router-dom-v5-compat';
6+
import { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src/extensions/console-types';
7+
import AutocompleteInput from '@console/internal/components/autocomplete';
8+
9+
type DataViewLabelFilterProps<TData> = {
10+
data: TData[];
11+
title: string;
12+
filterId: string;
13+
onChange?: (key: string, selectedValues: string) => void;
14+
showToolbarItem?: boolean;
15+
};
16+
17+
export const DataViewLabelFilter = <TData extends K8sResourceCommon = K8sResourceCommon>({
18+
data,
19+
title,
20+
filterId,
21+
onChange,
22+
showToolbarItem,
23+
}: DataViewLabelFilterProps<TData>) => {
24+
const { t } = useTranslation();
25+
26+
const [searchParams] = useSearchParams();
27+
const [labelInputText, setLabelInputText] = React.useState('');
28+
const labelSelection = searchParams.get(filterId)?.split(',') ?? [];
29+
30+
const applyLabelFilters = (values: string[]) => {
31+
setLabelInputText('');
32+
onChange?.(filterId, values.join(','));
33+
};
34+
35+
return (
36+
<ToolbarFilter
37+
categoryName={title}
38+
labels={labelSelection}
39+
showToolbarItem={showToolbarItem}
40+
deleteLabel={(_category, label: string) => {
41+
setLabelInputText('');
42+
applyLabelFilters(_.difference(labelSelection, [label]));
43+
}}
44+
deleteLabelGroup={() => {
45+
setLabelInputText('');
46+
applyLabelFilters([]);
47+
}}
48+
>
49+
<div className="pf-v6-c-input-group co-filter-group">
50+
<AutocompleteInput
51+
color="purple"
52+
onSuggestionSelect={(selected) => {
53+
applyLabelFilters(_.uniq([...labelSelection, selected]));
54+
}}
55+
showSuggestions
56+
textValue={labelInputText}
57+
setTextValue={setLabelInputText}
58+
placeholder={t('public~Filter by label')}
59+
data={data}
60+
/>
61+
</div>
62+
</ToolbarFilter>
63+
);
64+
};
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import * as React from 'react';
2+
import {
3+
ResponsiveAction,
4+
ResponsiveActions,
5+
SkeletonTableBody,
6+
} from '@patternfly/react-component-groups';
7+
import { Bullseye, Pagination, Tooltip } from '@patternfly/react-core';
8+
import {
9+
DataView,
10+
DataViewState,
11+
DataViewTable,
12+
DataViewTextFilter,
13+
DataViewToolbar,
14+
} from '@patternfly/react-data-view';
15+
import DataViewFilters from '@patternfly/react-data-view/dist/cjs/DataViewFilters';
16+
import { ColumnsIcon } from '@patternfly/react-icons';
17+
import { InnerScrollContainer, Tbody, Td, Tr } from '@patternfly/react-table';
18+
import { useTranslation } from 'react-i18next';
19+
import {
20+
ColumnLayout,
21+
K8sResourceCommon,
22+
} from '@console/dynamic-plugin-sdk/src/extensions/console-types';
23+
import { createColumnManagementModal } from '@console/internal/components/modals';
24+
import { TableColumn } from '@console/internal/module/k8s';
25+
import { DataViewLabelFilter } from './DataViewLabelFilter';
26+
import { ResourceFilters } from './types';
27+
import { useResourceDataViewData, GetDataViewRows } from './useResourceDataViewData';
28+
import { useResourceDataViewFilters } from './useResourceDataViewFilters';
29+
30+
export type ResourceDataViewProps<TData, TCustomRowData, TFilters> = {
31+
data: TData[];
32+
loaded: boolean;
33+
columns: TableColumn<TData>[];
34+
columnLayout?: ColumnLayout;
35+
columnManagementID?: string;
36+
initialFilters: TFilters;
37+
additionalFilterNodes?: React.ReactNode[];
38+
matchesAdditionalFilters?: (resource: TData, filters: TFilters) => boolean;
39+
getDataViewRows: GetDataViewRows<TData, TCustomRowData>;
40+
customRowData?: TCustomRowData;
41+
showNamespaceOverride?: boolean;
42+
hideNameLabelFilters?: boolean;
43+
hideLabelFilter?: boolean;
44+
hideColumnManagement?: boolean;
45+
};
46+
47+
/**
48+
* Console DataView component based on PatternFly DataView.
49+
*/
50+
export const ResourceDataView = <
51+
TData extends K8sResourceCommon = K8sResourceCommon,
52+
TCustomRowData = any,
53+
TFilters extends ResourceFilters = ResourceFilters
54+
>({
55+
data,
56+
loaded,
57+
columns,
58+
columnLayout,
59+
columnManagementID,
60+
initialFilters,
61+
additionalFilterNodes,
62+
matchesAdditionalFilters,
63+
getDataViewRows,
64+
customRowData,
65+
showNamespaceOverride,
66+
hideNameLabelFilters,
67+
hideLabelFilter,
68+
hideColumnManagement,
69+
}: ResourceDataViewProps<TData, TCustomRowData, TFilters>) => {
70+
const { t } = useTranslation();
71+
72+
const { filters, onSetFilters, clearAllFilters, filteredData } = useResourceDataViewFilters<
73+
TData,
74+
TFilters
75+
>({
76+
data,
77+
initialFilters,
78+
matchesAdditionalFilters,
79+
});
80+
81+
const { dataViewColumns, dataViewRows, pagination } = useResourceDataViewData<
82+
TData,
83+
TCustomRowData
84+
>({
85+
columns,
86+
filteredData,
87+
getDataViewRows,
88+
showNamespaceOverride,
89+
columnManagementID,
90+
customRowData,
91+
});
92+
93+
const bodyLoading = React.useMemo(
94+
() => <SkeletonTableBody rowsCount={5} columnsCount={dataViewColumns.length} />,
95+
[dataViewColumns.length],
96+
);
97+
98+
const bodyEmpty = React.useMemo(
99+
() => (
100+
<Tbody>
101+
<Tr>
102+
<Td colSpan={dataViewColumns.length}>
103+
<Bullseye>{t('public~No Pods found')}</Bullseye>
104+
</Td>
105+
</Tr>
106+
</Tbody>
107+
),
108+
[t, dataViewColumns.length],
109+
);
110+
111+
const activeState = React.useMemo(() => {
112+
if (!loaded) {
113+
return DataViewState.loading;
114+
}
115+
if (filteredData.length === 0) {
116+
return DataViewState.empty;
117+
}
118+
return undefined;
119+
}, [filteredData.length, loaded]);
120+
121+
const dataViewFiltersNodes = React.useMemo<React.ReactNode[]>(() => {
122+
const basicFilters = [
123+
!hideNameLabelFilters && (
124+
<DataViewTextFilter key="name" filterId="name" title={t('public~Name')} />
125+
),
126+
!hideNameLabelFilters && hideLabelFilter !== true && (
127+
<DataViewLabelFilter key="labels" filterId="label" title={t('public~Label')} data={data} />
128+
),
129+
];
130+
131+
return additionalFilterNodes?.length > 0
132+
? [...additionalFilterNodes, ...basicFilters]
133+
: basicFilters;
134+
135+
// Can't use data in the deps array as it will recompute the filters and will cause the selected category to reset
136+
// eslint-disable-next-line react-hooks/exhaustive-deps
137+
}, [additionalFilterNodes, t]);
138+
139+
return (
140+
<DataView activeState={activeState}>
141+
<DataViewToolbar
142+
filters={
143+
<DataViewFilters values={filters} onChange={(_e, values) => onSetFilters(values)}>
144+
{dataViewFiltersNodes}
145+
</DataViewFilters>
146+
}
147+
clearAllFilters={clearAllFilters}
148+
actions={
149+
!hideColumnManagement && (
150+
<ResponsiveActions breakpoint="lg">
151+
<ResponsiveAction
152+
isPersistent
153+
variant="plain"
154+
onClick={() =>
155+
createColumnManagementModal({
156+
columnLayout,
157+
noLimit: true,
158+
})
159+
}
160+
aria-label={t('public~Column management')}
161+
data-test="manage-columns"
162+
>
163+
<Tooltip content={t('public~Manage columns')} trigger="mouseenter">
164+
<ColumnsIcon />
165+
</Tooltip>
166+
</ResponsiveAction>
167+
</ResponsiveActions>
168+
)
169+
}
170+
pagination={<Pagination itemCount={filteredData.length} {...pagination} />}
171+
/>
172+
<InnerScrollContainer>
173+
<DataViewTable
174+
columns={dataViewColumns}
175+
rows={dataViewRows}
176+
bodyStates={{ empty: bodyEmpty, loading: bodyLoading }}
177+
gridBreakPoint=""
178+
variant="compact"
179+
/>
180+
</InnerScrollContainer>
181+
</DataView>
182+
);
183+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { DataViewTh, DataViewTd } from '@patternfly/react-data-view';
2+
import { SortByDirection } from '@patternfly/react-table';
3+
import { K8sResourceCommon } from '@console/dynamic-plugin-sdk/src/extensions/console-types';
4+
5+
export type ResourceFilters = {
6+
name: string;
7+
labels: string;
8+
};
9+
10+
export type ResourceDataViewColumn<
11+
TData extends K8sResourceCommon = K8sResourceCommon
12+
> = DataViewTh & {
13+
id: string;
14+
title: string;
15+
sortFunction?: string | ((filteredData: TData[], sortDirection: SortByDirection) => TData[]);
16+
};
17+
18+
export type ResourceDataViewRow = DataViewTd[];

0 commit comments

Comments
 (0)