mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: count per lifecycle stage (#9845)
Show count per stage, and include count if flags are filtered.
This commit is contained in:
parent
bd78a75177
commit
3ac087e0f6
@ -0,0 +1,104 @@
|
||||
import { type MockedFunction, vi } from 'vitest';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { LifecycleFilters } from './LifecycleFilters';
|
||||
import { useLifecycleCount } from 'hooks/api/getters/useLifecycleCount/useLifecycleCount';
|
||||
|
||||
vi.mock('hooks/api/getters/useLifecycleCount/useLifecycleCount');
|
||||
|
||||
const mockUseLifecycleCount = useLifecycleCount as MockedFunction<
|
||||
typeof useLifecycleCount
|
||||
>;
|
||||
|
||||
describe('LifecycleFilters', () => {
|
||||
beforeEach(() => {
|
||||
mockUseLifecycleCount.mockReturnValue({
|
||||
lifecycleCount: {
|
||||
initial: 1,
|
||||
preLive: 2,
|
||||
live: 3,
|
||||
completed: 4,
|
||||
archived: 0,
|
||||
},
|
||||
error: undefined,
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders labels without count if lifecycle count is not available', async () => {
|
||||
mockUseLifecycleCount.mockReturnValue({
|
||||
lifecycleCount: undefined,
|
||||
error: undefined,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<LifecycleFilters state={{}} onChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(getByText('All flags')).toBeInTheDocument();
|
||||
expect(getByText('Develop')).toBeInTheDocument();
|
||||
expect(getByText('Rollout production')).toBeInTheDocument();
|
||||
expect(getByText('Cleanup')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all stages with correct counts when no total provided', () => {
|
||||
const { getByText } = render(
|
||||
<LifecycleFilters state={{}} onChange={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(getByText('All flags (10)')).toBeInTheDocument();
|
||||
expect(getByText('Develop (2)')).toBeInTheDocument();
|
||||
expect(getByText('Rollout production (3)')).toBeInTheDocument();
|
||||
expect(getByText('Cleanup (4)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dynamic label when total matches count', () => {
|
||||
const total = 3;
|
||||
const { getByText } = render(
|
||||
<LifecycleFilters
|
||||
state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
|
||||
onChange={vi.fn()}
|
||||
total={total}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Rollout production (3)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dynamic label when total does not match count', () => {
|
||||
const total = 2;
|
||||
const { getByText } = render(
|
||||
<LifecycleFilters
|
||||
state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
|
||||
onChange={vi.fn()}
|
||||
total={total}
|
||||
/>,
|
||||
);
|
||||
expect(getByText('Rollout production (2 of 3)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('will apply a correct filter for each stage', async () => {
|
||||
const onChange = vi.fn();
|
||||
const { getByText } = render(
|
||||
<LifecycleFilters state={{}} onChange={onChange} />,
|
||||
);
|
||||
|
||||
await userEvent.click(getByText('Develop (2)'));
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
lifecycle: { operator: 'IS', values: ['pre-live'] },
|
||||
});
|
||||
|
||||
await userEvent.click(getByText('Rollout production (3)'));
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
lifecycle: { operator: 'IS', values: ['live'] },
|
||||
});
|
||||
|
||||
await userEvent.click(getByText('Cleanup (4)'));
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
lifecycle: { operator: 'IS', values: ['completed'] },
|
||||
});
|
||||
|
||||
await userEvent.click(getByText('All flags (10)'));
|
||||
expect(onChange).toHaveBeenCalledWith({ lifecycle: null });
|
||||
});
|
||||
});
|
@ -2,6 +2,8 @@ import { Box, Chip, styled } from '@mui/material';
|
||||
import type { FC } from 'react';
|
||||
import type { FilterItemParamHolder } from '../../../filter/Filters/Filters';
|
||||
import type { LifecycleStage } from '../../FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage';
|
||||
import { useLifecycleCount } from 'hooks/api/getters/useLifecycleCount/useLifecycleCount';
|
||||
import type { FeatureLifecycleCountSchema } from 'openapi';
|
||||
|
||||
const StyledChip = styled(Chip, {
|
||||
shouldForwardProp: (prop) => prop !== 'isActive',
|
||||
@ -47,11 +49,33 @@ const lifecycleOptions: {
|
||||
{ label: 'Cleanup', value: 'completed' },
|
||||
];
|
||||
|
||||
const getStageCount = (
|
||||
lifecycle: LifecycleStage['name'] | null,
|
||||
lifecycleCount?: FeatureLifecycleCountSchema,
|
||||
) => {
|
||||
if (!lifecycleCount) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (lifecycle === null) {
|
||||
return (
|
||||
(lifecycleCount.initial || 0) +
|
||||
(lifecycleCount.preLive || 0) +
|
||||
(lifecycleCount.live || 0) +
|
||||
(lifecycleCount.completed || 0)
|
||||
);
|
||||
}
|
||||
|
||||
const key = lifecycle === 'pre-live' ? 'preLive' : lifecycle;
|
||||
return lifecycleCount[key];
|
||||
};
|
||||
|
||||
export const LifecycleFilters: FC<ILifecycleFiltersProps> = ({
|
||||
state,
|
||||
onChange,
|
||||
total,
|
||||
}) => {
|
||||
const { lifecycleCount } = useLifecycleCount();
|
||||
const current = state.lifecycle?.values ?? [];
|
||||
|
||||
return (
|
||||
@ -59,10 +83,11 @@ export const LifecycleFilters: FC<ILifecycleFiltersProps> = ({
|
||||
{lifecycleOptions.map(({ label, value }) => {
|
||||
const isActive =
|
||||
value === null ? !state.lifecycle : current.includes(value);
|
||||
const count = getStageCount(value, lifecycleCount);
|
||||
const dynamicLabel =
|
||||
isActive && Number.isInteger(total)
|
||||
? `${label} (${total})`
|
||||
: label;
|
||||
? `${label} (${total === count ? total : `${total} of ${count}`})`
|
||||
: `${label}${count !== undefined ? ` (${count})` : ''}`;
|
||||
|
||||
const handleClick = () =>
|
||||
onChange(
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import useSWR from 'swr';
|
||||
import type { FeatureLifecycleCountSchema } from 'openapi';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
export const useLifecycleCount = () => {
|
||||
const { data, error } = useSWR<FeatureLifecycleCountSchema>(
|
||||
formatApiPath('api/admin/lifecycle/count'),
|
||||
fetcher,
|
||||
);
|
||||
|
||||
return {
|
||||
lifecycleCount: data,
|
||||
error,
|
||||
loading: !error && !data,
|
||||
};
|
||||
};
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('Lifecycle count'))
|
||||
.then((res) => res.json());
|
||||
};
|
Loading…
Reference in New Issue
Block a user