mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
feat(frontend): quick filters on project overview
This commit is contained in:
parent
71b0d424b8
commit
86f7806dd0
@ -37,6 +37,30 @@ const setupApi = () => {
|
||||
{ id: 1, name: 'AuthorA' },
|
||||
{ id: 2, name: 'AuthorB' },
|
||||
]);
|
||||
testServerRoute(server, '/api/admin/projects/default/status', {
|
||||
activityCountByDate: [],
|
||||
resources: {
|
||||
members: 0,
|
||||
apiTokens: 0,
|
||||
segments: 0,
|
||||
},
|
||||
health: {
|
||||
current: 0,
|
||||
},
|
||||
technicalDebt: {
|
||||
current: 0,
|
||||
},
|
||||
lifecycleSummary: {
|
||||
initial: { currentFlags: 1, averageDays: null },
|
||||
preLive: { currentFlags: 2, averageDays: null },
|
||||
live: { currentFlags: 3, averageDays: null },
|
||||
completed: { currentFlags: 1, averageDays: null },
|
||||
archived: { currentFlags: 0, last30Days: 0 },
|
||||
},
|
||||
staleFlags: {
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
test('filters by flag type', async () => {
|
||||
@ -208,7 +232,7 @@ test('Project is not onboarded', async () => {
|
||||
await screen.findByText('Welcome to your project');
|
||||
});
|
||||
|
||||
test('renders lifecycle filters', async () => {
|
||||
test('renders lifecycle quick filters', async () => {
|
||||
setupApi();
|
||||
|
||||
render(
|
||||
@ -227,14 +251,8 @@ test('renders lifecycle filters', async () => {
|
||||
},
|
||||
);
|
||||
|
||||
const addFilter = await screen.findByText('Filter');
|
||||
fireEvent.click(addFilter);
|
||||
|
||||
const lifecycleFilter = await screen.findByText('Lifecycle stage');
|
||||
fireEvent.click(lifecycleFilter);
|
||||
|
||||
await screen.findByText('Define');
|
||||
await screen.findByText('Develop');
|
||||
await screen.findByText('Rollout production');
|
||||
await screen.findByText('Cleanup');
|
||||
await screen.findByText(/All flags/);
|
||||
await screen.findByText(/Develop/);
|
||||
await screen.findByText(/Rollout production/);
|
||||
await screen.findByText(/Cleanup/);
|
||||
});
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
PlaceholderFeatureToggleCell,
|
||||
} from './FeatureToggleCell/FeatureToggleCell.tsx';
|
||||
import { ProjectOverviewFilters } from './ProjectOverviewFilters.tsx';
|
||||
import { ProjectLifecycleFilters } from './ProjectLifecycleFilters.tsx';
|
||||
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility.ts';
|
||||
import { TableEmptyState } from './TableEmptyState/TableEmptyState.tsx';
|
||||
import { useRowActions } from './hooks/useRowActions.tsx';
|
||||
@ -41,7 +42,7 @@ import {
|
||||
useProjectFeatureSearchActions,
|
||||
} from './useProjectFeatureSearch.ts';
|
||||
import { AvatarCell } from './AvatarCell.tsx';
|
||||
import { styled } from '@mui/material';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||
import { ConnectSdkDialog } from '../../../onboarding/dialog/ConnectSdkDialog.tsx';
|
||||
import { ProjectOnboarding } from '../../../onboarding/flow/ProjectOnboarding.tsx';
|
||||
@ -577,6 +578,14 @@ export const ProjectFeatureToggles = ({
|
||||
onChange={setTableState}
|
||||
state={filterState}
|
||||
/>
|
||||
<Box sx={{ marginRight: 'auto' }} data-test>
|
||||
<ProjectLifecycleFilters
|
||||
projectId={projectId}
|
||||
state={filterState}
|
||||
onChange={setTableState}
|
||||
total={loading ? undefined : total}
|
||||
/>
|
||||
</Box>
|
||||
<ButtonGroup>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_FEATURE}
|
||||
|
@ -0,0 +1,133 @@
|
||||
import { Box, Chip, styled } from '@mui/material';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import type { FilterItemParamHolder } from '../../../filter/Filters/Filters.tsx';
|
||||
import type { LifecycleStage } from '../../../feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx';
|
||||
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
|
||||
|
||||
const StyledChip = styled(Chip, {
|
||||
shouldForwardProp: (prop) => prop !== 'isActive',
|
||||
})<{
|
||||
isActive?: boolean;
|
||||
}>(({ theme, isActive = false }) => ({
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
padding: theme.spacing(0.5),
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
height: 'auto',
|
||||
...(isActive && {
|
||||
backgroundColor: theme.palette.secondary.light,
|
||||
fontWeight: 'bold',
|
||||
borderColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.main,
|
||||
}),
|
||||
':focus-visible': {
|
||||
outline: `1px solid ${theme.palette.primary.main}`,
|
||||
borderColor: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProjectLifecycleFiltersProps {
|
||||
projectId: string;
|
||||
state: FilterItemParamHolder;
|
||||
onChange: (value: FilterItemParamHolder) => void;
|
||||
total?: number;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Wrapper = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
minHeight: theme.spacing(7),
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const lifecycleOptions: {
|
||||
label: string;
|
||||
value: LifecycleStage['name'] | null;
|
||||
}[] = [
|
||||
{ label: 'All flags', value: null },
|
||||
{ label: 'Develop', value: 'pre-live' },
|
||||
{ label: 'Rollout production', value: 'live' },
|
||||
{ label: 'Cleanup', value: 'completed' },
|
||||
];
|
||||
|
||||
const getStageCount = (
|
||||
lifecycle: LifecycleStage['name'] | null,
|
||||
lifecycleSummary?: { [key: string]: { currentFlags: number } },
|
||||
) => {
|
||||
if (!lifecycleSummary) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (lifecycle === null) {
|
||||
return (
|
||||
(lifecycleSummary.initial?.currentFlags || 0) +
|
||||
(lifecycleSummary.preLive?.currentFlags || 0) +
|
||||
(lifecycleSummary.live?.currentFlags || 0) +
|
||||
(lifecycleSummary.completed?.currentFlags || 0)
|
||||
);
|
||||
}
|
||||
|
||||
const key = lifecycle === 'pre-live' ? 'preLive' : lifecycle;
|
||||
return lifecycleSummary[key]?.currentFlags;
|
||||
};
|
||||
|
||||
export const ProjectLifecycleFilters: FC<IProjectLifecycleFiltersProps> = ({
|
||||
projectId,
|
||||
state,
|
||||
onChange,
|
||||
total,
|
||||
children,
|
||||
}) => {
|
||||
const { data } = useProjectStatus(projectId);
|
||||
const lifecycleSummary = data?.lifecycleSummary;
|
||||
const current = state.lifecycle?.values ?? [];
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<StyledContainer>
|
||||
{lifecycleOptions.map(({ label, value }) => {
|
||||
const isActive =
|
||||
value === null
|
||||
? !state.lifecycle
|
||||
: current.includes(value);
|
||||
const count = getStageCount(value, lifecycleSummary);
|
||||
const dynamicLabel =
|
||||
isActive && Number.isInteger(total)
|
||||
? `${label} (${total === count ? total : `${total} of ${count}`})`
|
||||
: `${label}${count !== undefined ? ` (${count})` : ''}`;
|
||||
|
||||
const handleClick = () =>
|
||||
onChange(
|
||||
value === null
|
||||
? { lifecycle: null }
|
||||
: {
|
||||
lifecycle: {
|
||||
operator: 'IS',
|
||||
values: [value],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledChip
|
||||
data-loading
|
||||
key={label}
|
||||
label={dynamicLabel}
|
||||
variant='outlined'
|
||||
isActive={isActive}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
{children}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
@ -118,19 +118,6 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
|
||||
singularOperators: ['IS'],
|
||||
pluralOperators: ['IS_ANY_OF'],
|
||||
},
|
||||
{
|
||||
label: 'Lifecycle stage',
|
||||
icon: 'model_training',
|
||||
options: [
|
||||
{ label: 'Define', value: 'initial' },
|
||||
{ label: 'Develop', value: 'pre-live' },
|
||||
{ label: 'Rollout production', value: 'live' },
|
||||
{ label: 'Cleanup', value: 'completed' },
|
||||
],
|
||||
filterKey: 'lifecycle',
|
||||
singularOperators: ['IS', 'IS_NOT'],
|
||||
pluralOperators: ['IS_ANY_OF', 'IS_NONE_OF'],
|
||||
},
|
||||
];
|
||||
|
||||
setAvailableFilters(availableFilters);
|
||||
|
Loading…
Reference in New Issue
Block a user