mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +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 { FC } from 'react';
|
||||||
import type { FilterItemParamHolder } from '../../../filter/Filters/Filters';
|
import type { FilterItemParamHolder } from '../../../filter/Filters/Filters';
|
||||||
import type { LifecycleStage } from '../../FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage';
|
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, {
|
const StyledChip = styled(Chip, {
|
||||||
shouldForwardProp: (prop) => prop !== 'isActive',
|
shouldForwardProp: (prop) => prop !== 'isActive',
|
||||||
@ -47,11 +49,33 @@ const lifecycleOptions: {
|
|||||||
{ label: 'Cleanup', value: 'completed' },
|
{ 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> = ({
|
export const LifecycleFilters: FC<ILifecycleFiltersProps> = ({
|
||||||
state,
|
state,
|
||||||
onChange,
|
onChange,
|
||||||
total,
|
total,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { lifecycleCount } = useLifecycleCount();
|
||||||
const current = state.lifecycle?.values ?? [];
|
const current = state.lifecycle?.values ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -59,10 +83,11 @@ export const LifecycleFilters: FC<ILifecycleFiltersProps> = ({
|
|||||||
{lifecycleOptions.map(({ label, value }) => {
|
{lifecycleOptions.map(({ label, value }) => {
|
||||||
const isActive =
|
const isActive =
|
||||||
value === null ? !state.lifecycle : current.includes(value);
|
value === null ? !state.lifecycle : current.includes(value);
|
||||||
|
const count = getStageCount(value, lifecycleCount);
|
||||||
const dynamicLabel =
|
const dynamicLabel =
|
||||||
isActive && Number.isInteger(total)
|
isActive && Number.isInteger(total)
|
||||||
? `${label} (${total})`
|
? `${label} (${total === count ? total : `${total} of ${count}`})`
|
||||||
: label;
|
: `${label}${count !== undefined ? ` (${count})` : ''}`;
|
||||||
|
|
||||||
const handleClick = () =>
|
const handleClick = () =>
|
||||||
onChange(
|
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