Skip to content
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

Add status filter for KubeCRDs #1769

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/components/App/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ function HomeComponent(props: HomeComponentProps) {
<SectionFilterHeader
title={t('All Clusters')}
noNamespaceFilter
noStatusFilter
headerStyle="subsection"
/>
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/App/Notifications/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default function NotificationList() {
<SectionFilterHeader
title={t('translation|Notifications')}
noNamespaceFilter
noStatusFilter
actions={[<NotificationActionMenu />]}
/>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ export function PluginSettingsPure(props: PluginSettingsPureProps) {
return (
<>
<SectionBox
title={<SectionFilterHeader title={t('translation|Plugins')} noNamespaceFilter />}
title={
<SectionFilterHeader title={t('translation|Plugins')} noNamespaceFilter noStatusFilter />
}
>
<SimpleTable
columns={[
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/common/Resource/ResourceListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export default function ResourceListView(
);
const withNamespaceFilter = (props as ResourceListViewWithResourceClassProps).resourceClass
?.isNamespaced;

const withStatusFilter =
(props as ResourceListViewWithResourceClassProps).resourceClass?.status !== undefined;
return (
<SectionBox
title={
Expand All @@ -45,6 +46,7 @@ export default function ResourceListView(
/>,
]}
noNamespaceFilter={!withNamespaceFilter}
noStatusFilter={!withStatusFilter}
{...headerProps}
/>
) : (
Expand Down
97 changes: 52 additions & 45 deletions frontend/src/components/common/SectionFilterHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ import { resetFilter, setNamespaceFilter, setSearchFilter } from '../../redux/fi
import { useTypedSelector } from '../../redux/reducers/reducers';
import { NamespacesAutocomplete } from './NamespacesAutocomplete';
import SectionHeader, { SectionHeaderProps } from './SectionHeader';
import { StatusesAutocomplete } from './StatusFilter';

export interface SectionFilterHeaderProps extends SectionHeaderProps {
noNamespaceFilter?: boolean;
noStatusFilter?: boolean;
noSearch?: boolean;
preRenderFromFilterActions?: React.ReactNode[];
}

export default function SectionFilterHeader(props: SectionFilterHeaderProps) {
const {
noNamespaceFilter = false,
noStatusFilter = false,
noSearch = false,
actions: propsActions = [],
preRenderFromFilterActions,
Expand All @@ -34,6 +37,7 @@ export default function SectionFilterHeader(props: SectionFilterHeaderProps) {
const location = useLocation();
const history = useHistory();
const hasNamespaceFilters = !noNamespaceFilter && filter.namespaces.size > 0;
const hasStatusFilters = !noStatusFilter && filter.statuses.size > 0;
const hasSearch = !noSearch && !!filter.search;
const { t } = useTranslation();

Expand All @@ -58,7 +62,7 @@ export default function SectionFilterHeader(props: SectionFilterHeaderProps) {
}

useHotkeys('ctrl+shift+f', () => {
if (!noSearch || !noNamespaceFilter) {
if (!noSearch || !noNamespaceFilter || !noStatusFilter) {
setShowFilters({ show: true, userTriggered: true });
}
});
Expand Down Expand Up @@ -99,7 +103,7 @@ export default function SectionFilterHeader(props: SectionFilterHeaderProps) {
React.useEffect(() => {
setShowFilters(state => {
return {
show: hasSearch || hasNamespaceFilters || state.userTriggered,
show: hasSearch || hasNamespaceFilters || hasStatusFilters || state.userTriggered,
userTriggered: state.userTriggered,
};
});
Expand All @@ -110,54 +114,57 @@ export default function SectionFilterHeader(props: SectionFilterHeaderProps) {
actions.push(...preRenderFromFilterActions);
}

if (!noSearch) {
if (!showFilters.show) {
actions.push(
<IconButton
aria-label={t('Show filter')}
onClick={() => setShowFilters({ show: true, userTriggered: true })}
size="medium"
>
<Icon icon="mdi:filter-variant" />
</IconButton>
);
} else {
actions.push(
<Grid container alignItems="flex-end" justifyContent="flex-end" spacing={1} wrap="nowrap">
{!noNamespaceFilter && (
<Grid item>
<NamespacesAutocomplete />
</Grid>
)}
if (!showFilters.show) {
actions.push(
<IconButton
aria-label={t('translation|Show filter')}
onClick={() => setShowFilters({ show: true, userTriggered: true })}
size="medium"
>
<Icon icon="mdi:filter-variant" />
</IconButton>
);
} else {
actions.push(
<Grid container alignItems="flex-end" justifyContent="flex-end" spacing={1} wrap="nowrap">
{!noStatusFilter && (
<Grid item>
<TextField
id="standard-search"
label={t('Search')}
type="search"
InputLabelProps={{ shrink: true }}
InputProps={{ role: 'search' }}
placeholder={t('Filter')}
value={filter.search}
onChange={event => {
dispatch(setSearchFilter(event.target.value));
setShowFilters({ show: true, userTriggered: true });
}}
inputRef={focusedRef}
/>
<StatusesAutocomplete />
</Grid>
)}
{!noNamespaceFilter && (
<Grid item>
<Button
variant="contained"
endIcon={<Icon icon="mdi:filter-variant-remove" />}
onClick={resetFilters}
aria-controls="standard-search"
>
{t('Clear')}
</Button>
<NamespacesAutocomplete />
</Grid>
)}
<Grid item>
<TextField
id="standard-search"
label={t('translation|Search')}
type="search"
InputLabelProps={{ shrink: true }}
InputProps={{ role: 'search' }}
placeholder={t('Filter')}
value={filter.search}
onChange={event => {
dispatch(setSearchFilter(event.target.value));
setShowFilters({ show: true, userTriggered: true });
}}
inputRef={focusedRef}
/>
</Grid>
);
}
<Grid item>
<Button
variant="contained"
endIcon={<Icon icon="mdi:filter-variant-remove" />}
onClick={resetFilters}
aria-controls="standard-search"
>
{t('translation|Clear')}
</Button>
</Grid>
</Grid>
);
}

if (!!propsActions) {
Expand Down
141 changes: 141 additions & 0 deletions frontend/src/components/common/StatusFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Icon } from '@iconify/react';
import Autocomplete from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Checkbox from '@mui/material/Checkbox';
import ListItem from '@mui/material/ListItem';
import { useTheme } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { addQuery } from '../../helpers';
import { setStatusFilter } from '../../redux/filterSlice';
import { useTypedSelector } from '../../redux/reducers/reducers';

export interface PureStatusesAutocompleteProps {
statusNames: string[];
onChange: (event: React.ChangeEvent<{}>, newValue: string[]) => void;
filter?: { statuses: Set<string>; search: string };
}

export function PureStatusesAutocomplete({
statusNames,
onChange: onChangeFromProps,
filter,
}: PureStatusesAutocompleteProps) {
const theme = useTheme();
const { t } = useTranslation(['glossary', 'translation']);
const [statusInput, setStatusInput] = React.useState<string>('');
const maxStatusesChars = 12;

const onInputChange = (event: object, value: string, reason: string) => {
// For some reason, the AutoComplete component resets the text after a short
// delay, so we need to avoid that or the user won't be able to edit/use what they type.
if (reason !== 'reset') {
setStatusInput(value);
}
};

const onChange = (event: React.ChangeEvent<{}>, newValue: string[]) => {
// Now we reset the input so it won't show next to the selected namespaces.
setStatusInput('');
onChangeFromProps(event, newValue);
};

return (
<Autocomplete
multiple
id="statuses-filter"
autoComplete
options={statusNames}
onChange={onChange}
onInputChange={onInputChange}
inputValue={statusInput}
// We reverse the namespaces so the last chosen appear as the first in the label. This
// is useful since the label is ellipsized and this we get to see it change.
value={filter && [...filter.statuses.values()].reverse()}
renderOption={(props, option, { selected }) => (
<ListItem {...props}>
<Checkbox
icon={<Icon icon="mdi:checkbox-blank-outline" />}
checkedIcon={<Icon icon="mdi:check-box-outline" />}
style={{
color: selected ? theme.palette.primary.main : theme.palette.text.primary,
}}
checked={selected}
/>
{option}
</ListItem>
)}
renderTags={(tags: string[]) => {
if (tags.length === 0) {
return <Typography variant="body2">{t('translation|All statuses')}</Typography>;
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be added to the translation files. If you run the tests locally, I think it changes the files for you.

}

let statusesToShow = tags[0];
const joiner = ', ';
const joinerLength = joiner.length;
let joinnedStatuses = 1;

tags.slice(1).forEach(tag => {
if (statusesToShow.length + tag.length + joinerLength <= maxStatusesChars) {
statusesToShow += joiner + tag;
joinnedStatuses++;
}
});

return (
<Typography style={{ overflowWrap: 'anywhere' }}>
{statusesToShow.length > maxStatusesChars
? statusesToShow.slice(0, maxStatusesChars) + '…'
: statusesToShow}
{tags.length > joinnedStatuses && (
<>
<Box component="span" marginRight={1}>
,
</Box>
+<Box component="span" fontWeight="bold">{`+${tags.length - joinnedStatuses}`}</Box>
</>
)}
</Typography>
);
}}
renderInput={params => (
<Box width="15rem">
<TextField
{...params}
variant="standard"
label={t('Statuses')}
fullWidth
InputLabelProps={{ shrink: true }}
style={{ marginTop: 0 }}
placeholder={filter && [...filter.statuses.values()].length > 0 ? '' : 'Filter'}
/>
</Box>
)}
/>
);
}

export function StatusesAutocomplete() {
const history = useHistory();
const location = useLocation();
const dispatch = useDispatch();
const filter = useTypedSelector(state => state.filter);
const [statusNames, setStatusNames] = React.useState<string[]>([]);

// set status names
React.useEffect(() => {
const allowedStatuses = ['Running', 'Pending', 'Failed', 'Succeeded'];
setStatusNames(allowedStatuses);
}, []);

const onChange = (event: React.ChangeEvent<{}>, newValue: string[]) => {
addQuery({ status: newValue.join(' ') }, { status: '' }, history, location, '');
dispatch(setStatusFilter(newValue));
};

return <PureStatusesAutocomplete statusNames={statusNames} onChange={onChange} filter={filter} />;
}
1 change: 1 addition & 0 deletions frontend/src/components/common/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const checkExports = [
'SectionHeader',
'ShowHideLabel',
'SimpleTable',
'StatusFilter',
'Tabs',
'Terminal',
'TileChart',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export * from './ErrorPage';
export * from './ConfirmButton';
export { default as ConfirmButton } from './ConfirmButton';
export * from './NamespacesAutocomplete';
export * from './StatusFilter';
1 change: 1 addition & 0 deletions frontend/src/components/crd/CustomResourceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ export function CustomResourceListTable(props: CustomResourceTableProps) {
title={title}
headerProps={{
noNamespaceFilter: !crd.isNamespaced,
noStatusFilter: !crd.status,
}}
resourceClass={CRClass}
columns={cols}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/crd/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function CustomResourceDefinitionList() {
title={t('glossary|Custom Resources')}
headerProps={{
noNamespaceFilter: true,
noStatusFilter: true,
}}
resourceClass={CRD}
columns={[
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/cronjob/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ export default function CronJobDetails() {
error={CronJob.getErrorMessage(jobsError)}
hideColumns={['namespace']}
noNamespaceFilter
noStatusFilter
/>,
]
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ingress/ClassList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function IngressClassList() {
title={t('Ingress Classes')}
headerProps={{
noNamespaceFilter: true,
noStatusFilter: true,
}}
resourceClass={IngressClass}
columns={[
Expand Down