Skip to content

feat(ws): Add Empty State to Workspace list #268

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
50 changes: 16 additions & 34 deletions workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx
Original file line number Diff line number Diff line change
@@ -21,11 +21,6 @@ import {
ToolbarGroup,
ToolbarFilter,
ToolbarToggleGroup,
EmptyStateActions,
EmptyState,
EmptyStateFooter,
EmptyStateBody,
Button,
Bullseye,
} from '@patternfly/react-core';
import {
@@ -39,8 +34,9 @@ import {
ActionsColumn,
IActions,
} from '@patternfly/react-table';
import { CodeIcon, FilterIcon, SearchIcon } from '@patternfly/react-icons';
import { CodeIcon, FilterIcon } from '@patternfly/react-icons';
import { WorkspaceKind, WorkspaceKindsColumnNames } from '~/shared/types';
import EmptyStateWithClearFilters from 'shared/components/EmptyStateWithClearFilters';

export enum ActionType {
ViewDetails,
@@ -245,6 +241,12 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
[sortedWorkspaceKinds, onFilter],
);

const clearAllFilters = React.useCallback(() => {
setSearchNameValue('');
setStatusSelection('');
setSearchDescriptionValue('');
}, []);

// Set up name search input
const searchNameInput = React.useMemo(
() => (
@@ -490,27 +492,14 @@ export const WorkspaceKinds: React.FunctionComponent = () => {

const emptyState = React.useMemo(
() => (
<EmptyState headingLevel="h4" titleText="No results found" icon={SearchIcon}>
<EmptyStateBody>
No results match the filter criteria. Clear all filters and try again.
</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button
variant="link"
onClick={() => {
setSearchNameValue('');
setStatusSelection('');
setSearchDescriptionValue('');
}}
>
Clear all filters
</Button>
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
<EmptyStateWithClearFilters
title="No results found"
body="No results match the filter criteria. Clear all filters and try again."
onClearFilters={clearAllFilters} // Pass the local clearAllFilters function
colSpan={8} // Pass colSpan for table usage
/>
),
[],
[clearAllFilters],
);

// Actions
@@ -549,14 +538,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
</Content>
<br />
<Content style={{ display: 'flex', alignItems: 'flex-start', columnGap: '20px' }}>
<Toolbar
id="attribute-search-filter-toolbar"
clearAllFilters={() => {
setSearchNameValue('');
setStatusSelection('');
setSearchDescriptionValue('');
}}
>
<Toolbar id="attribute-search-filter-toolbar" clearAllFilters={clearAllFilters}>
<ToolbarContent>
<ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="xl">
<ToolbarGroup variant="filter-group">
Original file line number Diff line number Diff line change
@@ -7,13 +7,11 @@ import {
ToolbarContent,
Card,
CardHeader,
EmptyState,
EmptyStateBody,
CardBody,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
import { WorkspaceImage } from '~/shared/types';
import Filter, { FilteredColumn } from '~/shared/components/Filter';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import EmptyStateWithClearFilters from 'shared/components/EmptyStateWithClearFilters';

type WorkspaceCreationImageListProps = {
images: WorkspaceImage[];
@@ -28,13 +26,19 @@ export const WorkspaceCreationImageList: React.FunctionComponent<
const [workspaceImages, setWorkspaceImages] = useState<WorkspaceImage[]>(images);
const [filters, setFilters] = useState<FilteredColumn[]>([]);

const filterRef = React.useRef<FilterRef>(null);

const filterableColumns = useMemo(
() => ({
name: 'Name',
}),
[],
);

const clearAllFilters = useCallback(() => {
filterRef.current?.clearAll();
}, []);

const getFilteredWorkspaceImagesByLabels = useCallback(
(unfilteredImages: WorkspaceImage[]) =>
unfilteredImages.filter((image) =>
@@ -97,6 +101,7 @@ export const WorkspaceCreationImageList: React.FunctionComponent<
<Toolbar id="toolbar-group-types">
<ToolbarContent>
<Filter
ref={filterRef}
id="filter-workspace-images"
onFilter={setFilters}
columnNames={filterableColumns}
@@ -106,11 +111,11 @@ export const WorkspaceCreationImageList: React.FunctionComponent<
</PageSection>
<PageSection isFilled>
{workspaceImages.length === 0 && (
<EmptyState titleText="No results found" headingLevel="h4" icon={SearchIcon}>
<EmptyStateBody>
No results match the filter criteria. Clear all filters and try again.
</EmptyStateBody>
</EmptyState>
<EmptyStateWithClearFilters
title="No results found"
body="No results match the filter criteria. Clear all filters and try again."
onClearFilters={clearAllFilters}
/>
)}
{workspaceImages.length > 0 && (
<Gallery hasGutter aria-label="Selectable card container">
Original file line number Diff line number Diff line change
@@ -8,12 +8,10 @@ import {
ToolbarContent,
Card,
CardHeader,
EmptyState,
EmptyStateBody,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
import { WorkspaceKind } from '~/shared/types';
import Filter, { FilteredColumn } from '~/shared/components/Filter';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import EmptyStateWithClearFilters from 'shared/components/EmptyStateWithClearFilters';

type WorkspaceCreationKindListProps = {
allWorkspaceKinds: WorkspaceKind[];
@@ -28,6 +26,8 @@ export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreatio
}) => {
const [workspaceKinds, setWorkspaceKinds] = useState<WorkspaceKind[]>(allWorkspaceKinds);

const filterRef = React.useRef<FilterRef>(null);

const filterableColumns = useMemo(
() => ({
name: 'Name',
@@ -67,6 +67,10 @@ export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreatio
[filterableColumns, allWorkspaceKinds],
);

const clearAllFilters = useCallback(() => {
filterRef.current?.clearAll();
}, []);

const onChange = useCallback(
(event: React.FormEvent<HTMLInputElement>) => {
const newSelectedWorkspaceKind = workspaceKinds.find(
@@ -83,7 +87,8 @@ export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreatio
<Toolbar id="toolbar-group-types">
<ToolbarContent>
<Filter
id="filter-workspace-kinds"
ref={filterRef}
id="filter-workspace-images"
onFilter={onFilter}
columnNames={filterableColumns}
/>
@@ -92,11 +97,11 @@ export const WorkspaceCreationKindList: React.FunctionComponent<WorkspaceCreatio
</PageSection>
<PageSection isFilled>
{workspaceKinds.length === 0 && (
<EmptyState titleText="No results found" headingLevel="h4" icon={SearchIcon}>
<EmptyStateBody>
No results match the filter criteria. Clear all filters and try again.
</EmptyStateBody>
</EmptyState>
<EmptyStateWithClearFilters
title="No results found"
body="No results match the filter criteria. Clear all filters and try again."
onClearFilters={clearAllFilters}
/>
)}
{workspaceKinds.length > 0 && (
<Gallery hasGutter aria-label="Selectable card container">
Original file line number Diff line number Diff line change
@@ -7,13 +7,11 @@ import {
ToolbarContent,
Card,
CardHeader,
EmptyState,
EmptyStateBody,
CardBody,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
import { WorkspacePodConfig } from '~/shared/types';
import Filter, { FilteredColumn } from '~/shared/components/Filter';
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
import EmptyStateWithClearFilters from 'shared/components/EmptyStateWithClearFilters';

type WorkspaceCreationPodConfigListProps = {
podConfigs: WorkspacePodConfig[];
@@ -28,6 +26,8 @@ export const WorkspaceCreationPodConfigList: React.FunctionComponent<
const [workspacePodConfigs, setWorkspacePodConfigs] = useState<WorkspacePodConfig[]>(podConfigs);
const [filters, setFilters] = useState<FilteredColumn[]>([]);

const filterRef = React.useRef<FilterRef>(null);

const filterableColumns = useMemo(
() => ({
name: 'Name',
@@ -60,6 +60,10 @@ export const WorkspaceCreationPodConfigList: React.FunctionComponent<
[workspacePodConfigs, onSelect],
);

const clearAllFilters = useCallback(() => {
filterRef.current?.clearAll();
}, []);

useEffect(() => {
// Search name with search value
let filteredWorkspacePodConfigs = podConfigs;
@@ -103,7 +107,8 @@ export const WorkspaceCreationPodConfigList: React.FunctionComponent<
<Toolbar id="toolbar-group-types">
<ToolbarContent>
<Filter
id="filter-workspace-podConfigs"
ref={filterRef}
id="filter-workspace-images"
onFilter={setFilters}
columnNames={filterableColumns}
/>
@@ -112,11 +117,11 @@ export const WorkspaceCreationPodConfigList: React.FunctionComponent<
</PageSection>
<PageSection isFilled>
{workspacePodConfigs.length === 0 && (
<EmptyState titleText="No results found" headingLevel="h4" icon={SearchIcon}>
<EmptyStateBody>
No results match the filter criteria. Clear all filters and try again.
</EmptyStateBody>
</EmptyState>
<EmptyStateWithClearFilters
title="No results found"
body="No results match the filter criteria. Clear all filters and try again."
onClearFilters={clearAllFilters}
/>
)}
{workspacePodConfigs.length > 0 && (
<Gallery hasGutter aria-label="Selectable card container">
278 changes: 156 additions & 122 deletions workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import {
Content,
Brand,
Tooltip,
Bullseye,
} from '@patternfly/react-core';
import {
Table,
@@ -47,7 +48,8 @@ import { WorkspaceConnectAction } from '~/app/pages/Workspaces/WorkspaceConnectA
import { WorkspaceStartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal';
import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal';
import { WorkspaceStopActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal';
import Filter, { FilteredColumn } from 'shared/components/Filter';
import EmptyStateWithClearFilters from '~/shared/components/EmptyStateWithClearFilters'; // Import the new component
import Filter, { FilteredColumn, FilterRef } from 'shared/components/Filter'; // Import FilterRef
import { formatRam } from 'shared/utilities/WorkspaceUtils';

export enum ActionType {
@@ -198,19 +200,21 @@ export const Workspaces: React.FunctionComponent = () => {
> = {}; // Initialize the redirect status dictionary
workspaceRedirectStatus = buildWorkspaceRedirectStatus(workspaceKinds); // Populate the dictionary

// Table columns
const columnNames: WorkspacesColumnNames = {
redirectStatus: 'Redirect Status',
name: 'Name',
kind: 'Kind',
image: 'Image',
podConfig: 'Pod Config',
state: 'State',
homeVol: 'Home Vol',
cpu: 'CPU',
ram: 'Memory',
lastActivity: 'Last Activity',
};
const columnNames: WorkspacesColumnNames = React.useMemo(
() => ({
redirectStatus: 'Redirect Status',
name: 'Name',
kind: 'Kind',
image: 'Image',
podConfig: 'Pod Config',
state: 'State',
homeVol: 'Home Vol',
cpu: 'CPU',
ram: 'Memory',
lastActivity: 'Last Activity',
}),
[],
);

const filterableColumns = {
name: 'Name',
@@ -231,6 +235,8 @@ export const Workspaces: React.FunctionComponent = () => {
const [isActionAlertModalOpen, setIsActionAlertModalOpen] = React.useState(false);
const [activeActionType, setActiveActionType] = React.useState<ActionType | null>(null);

const filterRef = React.useRef<FilterRef>(null);

const selectWorkspace = React.useCallback(
(newSelectedWorkspace: Workspace | null) => {
if (selectedWorkspace?.name === newSelectedWorkspace?.name) {
@@ -254,41 +260,56 @@ export const Workspaces: React.FunctionComponent = () => {
expandedWorkspacesNames.includes(workspace.name);

// filter function to pass to the filter component
const onFilter = (filters: FilteredColumn[]) => {
// Search name with search value
let filteredWorkspaces = initialWorkspaces;
filters.forEach((filter) => {
let searchValueInput: RegExp;
try {
searchValueInput = new RegExp(filter.value, 'i');
} catch {
searchValueInput = new RegExp(filter.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
}

filteredWorkspaces = filteredWorkspaces.filter((workspace) => {
if (filter.value === '') {
return true;
const onFilter = React.useCallback(
(filters: FilteredColumn[]) => {
// Search name with search value
let filteredWorkspaces = initialWorkspaces;
filters.forEach((filter) => {
let searchValueInput: RegExp;
try {
searchValueInput = new RegExp(filter.value, 'i');
} catch {
searchValueInput = new RegExp(filter.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
}
switch (filter.columnName) {
case columnNames.name:
return workspace.name.search(searchValueInput) >= 0;
case columnNames.kind:
return workspace.kind.search(searchValueInput) >= 0;
case columnNames.image:
return workspace.options.imageConfig.search(searchValueInput) >= 0;
case columnNames.podConfig:
return workspace.options.podConfig.search(searchValueInput) >= 0;
case columnNames.state:
return WorkspaceState[workspace.status.state].search(searchValueInput) >= 0;
case columnNames.homeVol:
return workspace.podTemplate.volumes.home.search(searchValueInput) >= 0;
default:

filteredWorkspaces = filteredWorkspaces.filter((workspace) => {
if (filter.value === '') {
return true;
}
}
switch (filter.columnName) {
case columnNames.name:
return workspace.name.search(searchValueInput) >= 0;
case columnNames.kind:
return workspace.kind.search(searchValueInput) >= 0;
case columnNames.image:
return workspace.options.imageConfig.search(searchValueInput) >= 0;
case columnNames.podConfig:
return workspace.options.podConfig.search(searchValueInput) >= 0;
case columnNames.state:
return WorkspaceState[workspace.status.state].search(searchValueInput) >= 0;
case columnNames.homeVol:
return workspace.podTemplate.volumes.home.search(searchValueInput) >= 0;
default:
return true;
}
});
});
});
setWorkspaces(filteredWorkspaces);
};
setWorkspaces(filteredWorkspaces);
},
[initialWorkspaces, columnNames],
);

const emptyState = React.useMemo(
() => (
<EmptyStateWithClearFilters
title="No results found"
body="No results match the filter criteria. Clear all filters and try again."
onClearFilters={() => filterRef.current?.clearAll()}
colSpan={11}
/>
),
[],
);

// Column sorting

@@ -559,7 +580,12 @@ export const Workspaces: React.FunctionComponent = () => {
</Content>
<br />
<Content style={{ display: 'flex', alignItems: 'flex-start', columnGap: '20px' }}>
<Filter id="filter-workspaces" onFilter={onFilter} columnNames={filterableColumns} />
<Filter
ref={filterRef}
id="filter-workspaces"
onFilter={onFilter}
columnNames={filterableColumns}
/>
<Button variant="primary" ouiaId="Primary" onClick={createWorkspace}>
Create Workspace
</Button>
@@ -579,82 +605,90 @@ export const Workspaces: React.FunctionComponent = () => {
<Th screenReaderText="Primary action" />
</Tr>
</Thead>
{sortedWorkspaces.map((workspace, rowIndex) => (
<Tbody
id="workspaces-table-content"
key={rowIndex}
isExpanded={isWorkspaceExpanded(workspace)}
data-testid="table-body"
>
<Tr id={`workspaces-table-row-${rowIndex + 1}`}>
<Td
expand={{
rowIndex,
isExpanded: isWorkspaceExpanded(workspace),
onToggle: () =>
setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)),
}}
/>
<Td dataLabel={columnNames.redirectStatus}>
{workspaceRedirectStatus[workspace.kind]
? getRedirectStatusIcon(
workspaceRedirectStatus[workspace.kind]?.level,
workspaceRedirectStatus[workspace.kind]?.message ||
'No API response available',
)
: getRedirectStatusIcon(undefined, 'No API response available')}
</Td>
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
<Td dataLabel={columnNames.kind}>
{kindLogoDict[workspace.kind] ? (
<Tooltip content={workspace.kind}>
<Brand
src={kindLogoDict[workspace.kind]}
alt={workspace.kind}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
</Tooltip>
) : (
<Tooltip content={workspace.kind}>
<CodeIcon />
</Tooltip>
)}
</Td>
<Td dataLabel={columnNames.image}>{workspace.options.imageConfig}</Td>
<Td dataLabel={columnNames.podConfig}>{workspace.options.podConfig}</Td>
<Td dataLabel={columnNames.state}>
<Label color={stateColors[workspace.status.state]}>
{WorkspaceState[workspace.status.state]}
</Label>
</Td>
<Td dataLabel={columnNames.homeVol}>{workspace.podTemplate.volumes.home}</Td>
<Td dataLabel={columnNames.cpu}>{`${workspace.cpu}%`}</Td>
<Td dataLabel={columnNames.ram}>{formatRam(workspace.ram)}</Td>
<Td dataLabel={columnNames.lastActivity}>
<Timestamp
date={new Date(workspace.status.activity.lastActivity)}
tooltip={{ variant: TimestampTooltipVariant.default }}
>
1 hour ago
</Timestamp>
</Td>
<Td>
<WorkspaceConnectAction workspace={workspace} />
</Td>
<Td isActionCell data-testid="action-column">
<ActionsColumn
items={workspaceDefaultActions(workspace).map((action) => ({
...action,
'data-testid': `action-${action.id || ''}`,
}))}
{sortedWorkspaces.length > 0 &&
sortedWorkspaces.map((workspace, rowIndex) => (
<Tbody
id="workspaces-table-content"
key={rowIndex}
isExpanded={isWorkspaceExpanded(workspace)}
data-testid="table-body"
>
<Tr id={`workspaces-table-row-${rowIndex + 1}`}>
<Td
expand={{
rowIndex,
isExpanded: isWorkspaceExpanded(workspace),
onToggle: () =>
setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)),
}}
/>
</Td>
</Tr>
{isWorkspaceExpanded(workspace) && (
<ExpandedWorkspaceRow workspace={workspace} columnNames={columnNames} />
)}
</Tbody>
))}
<Td dataLabel={columnNames.redirectStatus}>
{workspaceRedirectStatus[workspace.kind]
? getRedirectStatusIcon(
workspaceRedirectStatus[workspace.kind]?.level,
workspaceRedirectStatus[workspace.kind]?.message ||
'No API response available',
)
: getRedirectStatusIcon(undefined, 'No API response available')}
</Td>
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
<Td dataLabel={columnNames.kind}>
{kindLogoDict[workspace.kind] ? (
<Tooltip content={workspace.kind}>
<Brand
src={kindLogoDict[workspace.kind]}
alt={workspace.kind}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
</Tooltip>
) : (
<Tooltip content={workspace.kind}>
<CodeIcon />
</Tooltip>
)}
</Td>
<Td dataLabel={columnNames.image}>{workspace.options.imageConfig}</Td>
<Td dataLabel={columnNames.podConfig}>{workspace.options.podConfig}</Td>
<Td dataLabel={columnNames.state}>
<Label color={stateColors[workspace.status.state]}>
{WorkspaceState[workspace.status.state]}
</Label>
</Td>
<Td dataLabel={columnNames.homeVol}>{workspace.podTemplate.volumes.home}</Td>
<Td dataLabel={columnNames.cpu}>{`${workspace.cpu}%`}</Td>
<Td dataLabel={columnNames.ram}>{formatRam(workspace.ram)}</Td>
<Td dataLabel={columnNames.lastActivity}>
<Timestamp
date={new Date(workspace.status.activity.lastActivity)}
tooltip={{ variant: TimestampTooltipVariant.default }}
>
1 hour ago
</Timestamp>
</Td>
<Td>
<WorkspaceConnectAction workspace={workspace} />
</Td>
<Td isActionCell data-testid="action-column">
<ActionsColumn
items={workspaceDefaultActions(workspace).map((action) => ({
...action,
'data-testid': `action-${action.id || ''}`,
}))}
/>
</Td>
</Tr>
{isWorkspaceExpanded(workspace) && (
<ExpandedWorkspaceRow workspace={workspace} columnNames={columnNames} />
)}
</Tbody>
))}
{sortedWorkspaces.length === 0 && (
<Tr>
<Td colSpan={12} id="empty-state-cell">
<Bullseye>{emptyState}</Bullseye>
</Td>
</Tr>
)}
</Table>
{isActionAlertModalOpen && chooseAlertModal()}
<DeleteModal
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';
import {
EmptyState,
EmptyStateBody,
EmptyStateFooter,
EmptyStateActions,
Button,
Bullseye,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';

interface EmptyStateWithClearFiltersProps {
title: string;
body: string;
onClearFilters: () => void;
colSpan?: number;
}

const EmptyStateWithClearFilters: React.FC<EmptyStateWithClearFiltersProps> = ({
title,
body,
onClearFilters,
colSpan,
}) =>
colSpan !== undefined ? (
<Bullseye>
<EmptyState headingLevel="h4" titleText={title} icon={SearchIcon}>
<EmptyStateBody>{body}</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button variant="link" onClick={onClearFilters}>
Clear all filters
</Button>
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
</Bullseye>
) : (
<EmptyState headingLevel="h4" titleText={title} icon={SearchIcon}>
<EmptyStateBody>{body}</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button variant="link" onClick={onClearFilters}>
Clear all filters
</Button>
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
);

export default EmptyStateWithClearFilters;
139 changes: 94 additions & 45 deletions workspaces/frontend/src/shared/components/Filter.tsx
Original file line number Diff line number Diff line change
@@ -28,7 +28,14 @@ export interface FilteredColumn {
value: string;
}

const Filter: React.FC<FilterProps> = ({ id, onFilter, columnNames }) => {
// Define the handle type that the parent will use to access the exposed function
export interface FilterRef {
clearAll: () => void;
}

// Use forwardRef to allow parents to get a ref to this component instance
const Filter = React.forwardRef<FilterRef, FilterProps>(({ id, onFilter, columnNames }, ref) => {
Filter.displayName = 'Filter';
const [activeFilter, setActiveFilter] = React.useState<FilteredColumn>({
columnName: Object.values(columnNames)[0],
value: '',
@@ -80,65 +87,108 @@ const Filter: React.FC<FilterProps> = ({ id, onFilter, columnNames }) => {
const onFilterToggleClick = React.useCallback(
(ev: React.MouseEvent) => {
ev.stopPropagation(); // Stop handleClickOutside from handling
if (filterMenuRef.current) {
const firstElement = filterMenuRef.current.querySelector('li > button:not(:disabled)');
setTimeout(() => {
const firstElement = filterMenuRef.current?.querySelector('li > button:not(:disabled)');
if (firstElement) {
(firstElement as HTMLElement).focus();
}
}
}, 0);
setIsFilterMenuOpen(!isFilterMenuOpen);
},
[isFilterMenuOpen],
);

const addFilter = React.useCallback(
const updateFilters = React.useCallback(
(filterObj: FilteredColumn) => {
const index = filters.findIndex((filter) => filter.columnName === filterObj.columnName);
const newFilters = filters;
if (index !== -1) {
newFilters[index] = filterObj;
} else {
setFilters((prevFilters) => {
const index = prevFilters.findIndex((filter) => filter.columnName === filterObj.columnName);
const newFilters = [...prevFilters];

if (filterObj.value === '') {
const updatedFilters = newFilters.filter(
(filter) => filter.columnName !== filterObj.columnName,
);
onFilter(updatedFilters);
return updatedFilters;
}
if (index !== -1) {
newFilters[index] = filterObj;
onFilter(newFilters);
return newFilters;
}
newFilters.push(filterObj);
}
setFilters(newFilters);
onFilter(newFilters);
return newFilters;
});
},
[filters],
[onFilter], // onFilter is a dependency
);

const onSearchChange = React.useCallback(
(value: string) => {
const newFilter = { columnName: activeFilter.columnName, value };
setSearchValue(value);
setActiveFilter(newFilter);
addFilter(newFilter);
onFilter(filters);
setActiveFilter((prevActiveFilter) => {
const newActiveFilter = { ...prevActiveFilter, value };
updateFilters(newActiveFilter);
return newActiveFilter;
});
},
[activeFilter.columnName, addFilter, filters, onFilter],
[updateFilters],
);

const onDeleteLabelGroup = React.useCallback(
(filter: FilteredColumn) => {
const newFilters = filters.filter((filter1) => filter1.columnName !== filter.columnName);
setFilters(newFilters);
setFilters((prevFilters) => {
const newFilters = prevFilters.filter(
(filter1) => filter1.columnName !== filter.columnName,
);
onFilter(newFilters);
return newFilters;
});
if (filter.columnName === activeFilter.columnName) {
setSearchValue('');
setActiveFilter((prevActiveFilter) => ({
...prevActiveFilter,
value: '',
}));
}
onFilter(newFilters);
},
[activeFilter.columnName, filters, onFilter],
[activeFilter.columnName, onFilter],
);

// Expose the clearAllFilters logic via the ref
const clearAllInternal = React.useCallback(() => {
setFilters([]);
setSearchValue('');
setActiveFilter({
columnName: Object.values(columnNames)[0],
value: '',
});
onFilter([]);
}, [columnNames, onFilter]);

React.useImperativeHandle(ref, () => ({
clearAll: clearAllInternal,
}));

const onFilterSelect = React.useCallback(
(itemId: string | number | undefined) => {
setIsFilterMenuOpen(!isFilterMenuOpen);
const index = filters.findIndex((filter) => filter.columnName === itemId);
setSearchValue(index === -1 ? '' : filters[index].value);
// Use the functional update form to toggle the state
setIsFilterMenuOpen((prevIsMenuOpen) => !prevIsMenuOpen); // Fix is here

const selectedColumnName = itemId ? itemId.toString() : Object.values(columnNames)[0];

// Find the existing filter value for the selected column, if any
const existingFilter = filters.find((filter) => filter.columnName === selectedColumnName);
const existingValue = existingFilter ? existingFilter.value : '';

setSearchValue(existingValue); // Set search input to the existing filter value
setActiveFilter({
columnName: itemId ? itemId.toString() : Object.values(columnNames)[0],
value: searchValue,
columnName: selectedColumnName,
value: existingValue, // Set the active filter value
});
},
[columnNames, filters, isFilterMenuOpen, searchValue],
[columnNames, filters],
);

const filterMenuToggle = React.useMemo(
@@ -191,11 +241,7 @@ const Filter: React.FC<FilterProps> = ({ id, onFilter, columnNames }) => {
return (
<Toolbar
id="attribute-search-filter-toolbar"
clearAllFilters={() => {
setFilters([]);
setSearchValue('');
onFilter([]);
}}
clearAllFilters={clearAllInternal} // Use the internal clear function
>
<ToolbarContent>
<ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="xl">
@@ -210,21 +256,24 @@ const Filter: React.FC<FilterProps> = ({ id, onFilter, columnNames }) => {
onClear={() => onSearchChange('')}
/>
</ToolbarItem>
{filters.map((filter) => (
<ToolbarFilter
key={`${filter.columnName}-filter`}
labels={filter.value !== '' ? [filter.value] : ['']}
deleteLabel={() => onDeleteLabelGroup(filter)}
deleteLabelGroup={() => onDeleteLabelGroup(filter)}
categoryName={filter.columnName}
>
{undefined}
</ToolbarFilter>
))}
{filters.map(
(filter) =>
filter.value !== '' && (
<ToolbarFilter
key={`${filter.columnName}-filter`}
labels={[filter.value]}
deleteLabel={() => onDeleteLabelGroup(filter)}
deleteLabelGroup={() => onDeleteLabelGroup(filter)}
categoryName={filter.columnName}
>
{undefined}
</ToolbarFilter>
),
)}
</ToolbarGroup>
</ToolbarToggleGroup>
</ToolbarContent>
</Toolbar>
);
};
});
export default Filter;