mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat(frontend): quick filters on project overview (#10638)
This commit is contained in:
		
							parent
							
								
									be4665f3f1
								
							
						
					
					
						commit
						4b42435590
					
				@ -1,9 +1,8 @@
 | 
			
		||||
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 '../../FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx';
 | 
			
		||||
import { useLifecycleCount } from 'hooks/api/getters/useLifecycleCount/useLifecycleCount';
 | 
			
		||||
import type { FeatureLifecycleCountSchema } from 'openapi';
 | 
			
		||||
import type { SxProps, Theme } from '@mui/material';
 | 
			
		||||
import type { ReactNode } from 'react';
 | 
			
		||||
import type { FilterItemParamHolder } from '../../filter/Filters/Filters.tsx';
 | 
			
		||||
import type { LifecycleStage } from '../../feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx';
 | 
			
		||||
 | 
			
		||||
const StyledChip = styled(Chip, {
 | 
			
		||||
    shouldForwardProp: (prop) => prop !== 'isActive',
 | 
			
		||||
@ -26,17 +25,18 @@ const StyledChip = styled(Chip, {
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface ILifecycleFiltersProps {
 | 
			
		||||
interface ILifecycleFiltersBaseProps {
 | 
			
		||||
    state: FilterItemParamHolder;
 | 
			
		||||
    onChange: (value: FilterItemParamHolder) => void;
 | 
			
		||||
    total?: number;
 | 
			
		||||
    children?: ReactNode;
 | 
			
		||||
    countData?: Record<LifecycleStage['name'], number>;
 | 
			
		||||
    sx?: SxProps<Theme>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Wrapper = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    padding: theme.spacing(1.5, 3, 0, 3),
 | 
			
		||||
    minHeight: theme.spacing(7),
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
@ -58,34 +58,13 @@ 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> = ({
 | 
			
		||||
export const LifecycleFilters = ({
 | 
			
		||||
    state,
 | 
			
		||||
    onChange,
 | 
			
		||||
    total,
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { lifecycleCount } = useLifecycleCount();
 | 
			
		||||
    countData,
 | 
			
		||||
}: ILifecycleFiltersBaseProps) => {
 | 
			
		||||
    const current = state.lifecycle?.values ?? [];
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@ -96,7 +75,7 @@ export const LifecycleFilters: FC<ILifecycleFiltersProps> = ({
 | 
			
		||||
                        value === null
 | 
			
		||||
                            ? !state.lifecycle
 | 
			
		||||
                            : current.includes(value);
 | 
			
		||||
                    const count = getStageCount(value, lifecycleCount);
 | 
			
		||||
                    const count = value ? countData?.[value] : total;
 | 
			
		||||
                    const dynamicLabel =
 | 
			
		||||
                        isActive && Number.isInteger(total)
 | 
			
		||||
                            ? `${label} (${total === count ? total : `${total} of ${count}`})`
 | 
			
		||||
@ -20,7 +20,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { focusable } from 'themes/themeStyles';
 | 
			
		||||
import { FeatureLifecycleCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import { FeatureToggleFilters } from './FeatureToggleFilters/FeatureToggleFilters.tsx';
 | 
			
		||||
import { FeaturesOverviewToggleFilters } from './FeaturesOverviewLifecycleFilters/FeaturesOverviewToggleFilters.tsx';
 | 
			
		||||
import { withTableState } from 'utils/withTableState';
 | 
			
		||||
import useLoading from 'hooks/useLoading';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
@ -29,7 +29,7 @@ import {
 | 
			
		||||
    useTableStateFilter,
 | 
			
		||||
} from './useGlobalFeatureSearch.ts';
 | 
			
		||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
 | 
			
		||||
import { LifecycleFilters } from './FeatureToggleFilters/LifecycleFilters.tsx';
 | 
			
		||||
import { FeaturesOverviewLifecycleFilters } from './FeaturesOverviewLifecycleFilters/FeaturesOverviewLifecycleFilters.tsx';
 | 
			
		||||
import { ExportFlags } from './ExportFlags.tsx';
 | 
			
		||||
import { createFeatureOverviewCell } from 'component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
 | 
			
		||||
import { AvatarCell } from 'component/project/Project/PaginatedProjectFeatureToggles/AvatarCell';
 | 
			
		||||
@ -290,7 +290,7 @@ export const FeatureToggleListTable: FC = () => {
 | 
			
		||||
                />
 | 
			
		||||
            }
 | 
			
		||||
        >
 | 
			
		||||
            <LifecycleFilters
 | 
			
		||||
            <FeaturesOverviewLifecycleFilters
 | 
			
		||||
                state={filterState}
 | 
			
		||||
                onChange={setTableState}
 | 
			
		||||
                total={loading ? undefined : total}
 | 
			
		||||
@ -303,8 +303,8 @@ export const FeatureToggleListTable: FC = () => {
 | 
			
		||||
                        id='globalFeatureFlags'
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </LifecycleFilters>
 | 
			
		||||
            <FeatureToggleFilters
 | 
			
		||||
            </FeaturesOverviewLifecycleFilters>
 | 
			
		||||
            <FeaturesOverviewToggleFilters
 | 
			
		||||
                onChange={setTableState}
 | 
			
		||||
                state={filterState}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { type MockedFunction, vi } from 'vitest';
 | 
			
		||||
import { render } from 'utils/testRenderer';
 | 
			
		||||
import userEvent from '@testing-library/user-event';
 | 
			
		||||
import { LifecycleFilters } from './LifecycleFilters.tsx';
 | 
			
		||||
import { FeaturesOverviewLifecycleFilters } from './FeaturesOverviewLifecycleFilters.tsx';
 | 
			
		||||
import { useLifecycleCount } from 'hooks/api/getters/useLifecycleCount/useLifecycleCount';
 | 
			
		||||
 | 
			
		||||
vi.mock('hooks/api/getters/useLifecycleCount/useLifecycleCount');
 | 
			
		||||
@ -33,7 +33,7 @@ describe('LifecycleFilters', () => {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const { getByText } = render(
 | 
			
		||||
            <LifecycleFilters state={{}} onChange={vi.fn()} />,
 | 
			
		||||
            <FeaturesOverviewLifecycleFilters state={{}} onChange={vi.fn()} />,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(getByText('All flags')).toBeInTheDocument();
 | 
			
		||||
@ -44,10 +44,10 @@ describe('LifecycleFilters', () => {
 | 
			
		||||
 | 
			
		||||
    it('renders all stages with correct counts when no total provided', () => {
 | 
			
		||||
        const { getByText } = render(
 | 
			
		||||
            <LifecycleFilters state={{}} onChange={vi.fn()} />,
 | 
			
		||||
            <FeaturesOverviewLifecycleFilters state={{}} onChange={vi.fn()} />,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(getByText('All flags (10)')).toBeInTheDocument();
 | 
			
		||||
        expect(getByText('All flags')).toBeInTheDocument();
 | 
			
		||||
        expect(getByText('Develop (2)')).toBeInTheDocument();
 | 
			
		||||
        expect(getByText('Rollout production (3)')).toBeInTheDocument();
 | 
			
		||||
        expect(getByText('Cleanup (4)')).toBeInTheDocument();
 | 
			
		||||
@ -56,7 +56,7 @@ describe('LifecycleFilters', () => {
 | 
			
		||||
    it('renders dynamic label when total matches count', () => {
 | 
			
		||||
        const total = 3;
 | 
			
		||||
        const { getByText } = render(
 | 
			
		||||
            <LifecycleFilters
 | 
			
		||||
            <FeaturesOverviewLifecycleFilters
 | 
			
		||||
                state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
 | 
			
		||||
                onChange={vi.fn()}
 | 
			
		||||
                total={total}
 | 
			
		||||
@ -68,7 +68,7 @@ describe('LifecycleFilters', () => {
 | 
			
		||||
    it('renders dynamic label when total does not match count', () => {
 | 
			
		||||
        const total = 2;
 | 
			
		||||
        const { getByText } = render(
 | 
			
		||||
            <LifecycleFilters
 | 
			
		||||
            <FeaturesOverviewLifecycleFilters
 | 
			
		||||
                state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
 | 
			
		||||
                onChange={vi.fn()}
 | 
			
		||||
                total={total}
 | 
			
		||||
@ -80,7 +80,7 @@ describe('LifecycleFilters', () => {
 | 
			
		||||
    it('will apply a correct filter for each stage', async () => {
 | 
			
		||||
        const onChange = vi.fn();
 | 
			
		||||
        const { getByText } = render(
 | 
			
		||||
            <LifecycleFilters state={{}} onChange={onChange} />,
 | 
			
		||||
            <FeaturesOverviewLifecycleFilters state={{}} onChange={onChange} />,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await userEvent.click(getByText('Develop (2)'));
 | 
			
		||||
@ -98,7 +98,7 @@ describe('LifecycleFilters', () => {
 | 
			
		||||
            lifecycle: { operator: 'IS', values: ['completed'] },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await userEvent.click(getByText('All flags (10)'));
 | 
			
		||||
        await userEvent.click(getByText('All flags'));
 | 
			
		||||
        expect(onChange).toHaveBeenCalledWith({ lifecycle: null });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
import type { FC, ReactNode } from 'react';
 | 
			
		||||
import { Box } from '@mui/material';
 | 
			
		||||
import type { FilterItemParamHolder } from '../../../filter/Filters/Filters.tsx';
 | 
			
		||||
import { useLifecycleCount } from 'hooks/api/getters/useLifecycleCount/useLifecycleCount';
 | 
			
		||||
import { LifecycleFilters } from 'component/common/LifecycleFilters/LifecycleFilters.tsx';
 | 
			
		||||
 | 
			
		||||
type FeaturesOverviewLifecycleFiltersProps = {
 | 
			
		||||
    state: FilterItemParamHolder;
 | 
			
		||||
    onChange: (value: FilterItemParamHolder) => void;
 | 
			
		||||
    total?: number;
 | 
			
		||||
    children?: ReactNode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const FeaturesOverviewLifecycleFilters: FC<
 | 
			
		||||
    FeaturesOverviewLifecycleFiltersProps
 | 
			
		||||
> = ({ state, onChange, total, children }) => {
 | 
			
		||||
    const { lifecycleCount } = useLifecycleCount();
 | 
			
		||||
    const countData = Object.entries(lifecycleCount || {}).reduce(
 | 
			
		||||
        (acc, [key, value]) => {
 | 
			
		||||
            acc[key === 'preLive' ? 'pre-live' : key] = value;
 | 
			
		||||
            return acc;
 | 
			
		||||
        },
 | 
			
		||||
        {} as Record<string, number>,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Box sx={(theme) => ({ padding: theme.spacing(1.5, 3, 0, 3) })}>
 | 
			
		||||
            <LifecycleFilters
 | 
			
		||||
                state={state}
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                total={total}
 | 
			
		||||
                countData={countData}
 | 
			
		||||
            >
 | 
			
		||||
                {children}
 | 
			
		||||
            </LifecycleFilters>
 | 
			
		||||
        </Box>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { screen } from '@testing-library/react';
 | 
			
		||||
import { render } from 'utils/testRenderer';
 | 
			
		||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
 | 
			
		||||
import { FeatureToggleFilters } from './FeatureToggleFilters.tsx';
 | 
			
		||||
import { FeaturesOverviewToggleFilters } from './FeaturesOverviewToggleFilters.tsx';
 | 
			
		||||
 | 
			
		||||
const server = testServerSetup();
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ test('should render projects filters when more than one project', async () => {
 | 
			
		||||
        ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    render(<FeatureToggleFilters onChange={() => {}} state={{}} />);
 | 
			
		||||
    render(<FeaturesOverviewToggleFilters onChange={() => {}} state={{}} />);
 | 
			
		||||
 | 
			
		||||
    await screen.findByText('Project');
 | 
			
		||||
});
 | 
			
		||||
@ -34,7 +34,7 @@ test('should not render projects filters when less than two project', async () =
 | 
			
		||||
        ],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    render(<FeatureToggleFilters onChange={() => {}} state={{}} />);
 | 
			
		||||
    render(<FeaturesOverviewToggleFilters onChange={() => {}} state={{}} />);
 | 
			
		||||
 | 
			
		||||
    expect(screen.queryByText('Projects')).not.toBeInTheDocument();
 | 
			
		||||
});
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { useEffect, useState, type VFC } from 'react';
 | 
			
		||||
import { type FC, useEffect, useState } from 'react';
 | 
			
		||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
 | 
			
		||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
 | 
			
		||||
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
 | 
			
		||||
@ -9,15 +9,14 @@ import {
 | 
			
		||||
} from 'component/filter/Filters/Filters';
 | 
			
		||||
import { formatTag } from 'utils/format-tag';
 | 
			
		||||
 | 
			
		||||
interface IFeatureToggleFiltersProps {
 | 
			
		||||
type FeaturesOverviewToggleFiltersProps = {
 | 
			
		||||
    state: FilterItemParamHolder;
 | 
			
		||||
    onChange: (value: FilterItemParamHolder) => void;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
 | 
			
		||||
    state,
 | 
			
		||||
    onChange,
 | 
			
		||||
}) => {
 | 
			
		||||
export const FeaturesOverviewToggleFilters: FC<
 | 
			
		||||
    FeaturesOverviewToggleFiltersProps
 | 
			
		||||
> = ({ state, onChange }) => {
 | 
			
		||||
    const { projects } = useProjects();
 | 
			
		||||
    const { segments } = useSegments();
 | 
			
		||||
    const { tags } = useAllTags();
 | 
			
		||||
@ -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';
 | 
			
		||||
@ -56,9 +57,9 @@ import { ImportModal } from '../Import/ImportModal.tsx';
 | 
			
		||||
import { IMPORT_BUTTON } from 'utils/testIds';
 | 
			
		||||
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
 | 
			
		||||
 | 
			
		||||
interface IPaginatedProjectFeatureTogglesProps {
 | 
			
		||||
type ProjectFeatureTogglesProps = {
 | 
			
		||||
    environments: string[];
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const formatEnvironmentColumnId = (environment: string) =>
 | 
			
		||||
    `environment:${environment}`;
 | 
			
		||||
@ -75,7 +76,6 @@ const Container = styled('div')(({ theme }) => ({
 | 
			
		||||
const FilterRow = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexFlow: 'row wrap',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@ const ButtonGroup = styled('div')(({ theme }) => ({
 | 
			
		||||
 | 
			
		||||
export const ProjectFeatureToggles = ({
 | 
			
		||||
    environments,
 | 
			
		||||
}: IPaginatedProjectFeatureTogglesProps) => {
 | 
			
		||||
}: ProjectFeatureTogglesProps) => {
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const { project } = useProjectOverview(projectId);
 | 
			
		||||
@ -577,6 +577,12 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
                            onChange={setTableState}
 | 
			
		||||
                            state={filterState}
 | 
			
		||||
                        />
 | 
			
		||||
                        <ProjectLifecycleFilters
 | 
			
		||||
                            projectId={projectId}
 | 
			
		||||
                            state={filterState}
 | 
			
		||||
                            onChange={setTableState}
 | 
			
		||||
                            total={loading ? undefined : total}
 | 
			
		||||
                        />
 | 
			
		||||
                        <ButtonGroup>
 | 
			
		||||
                            <PermissionIconButton
 | 
			
		||||
                                permission={UPDATE_FEATURE}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,64 @@
 | 
			
		||||
import type { FC, ReactNode } from 'react';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import type { FilterItemParamHolder } from '../../../filter/Filters/Filters.tsx';
 | 
			
		||||
import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectStatus';
 | 
			
		||||
import { LifecycleFilters } from 'component/common/LifecycleFilters/LifecycleFilters.tsx';
 | 
			
		||||
import { Box, useMediaQuery, useTheme } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
type ProjectLifecycleFiltersProps = {
 | 
			
		||||
    projectId: string;
 | 
			
		||||
    state: FilterItemParamHolder;
 | 
			
		||||
    onChange: (value: FilterItemParamHolder) => void;
 | 
			
		||||
    total?: number;
 | 
			
		||||
    children?: ReactNode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ProjectLifecycleFilters: FC<ProjectLifecycleFiltersProps> = ({
 | 
			
		||||
    projectId,
 | 
			
		||||
    state,
 | 
			
		||||
    onChange,
 | 
			
		||||
    total,
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { data } = useProjectStatus(projectId);
 | 
			
		||||
    const theme = useTheme();
 | 
			
		||||
    const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
 | 
			
		||||
    const lifecycleSummary = Object.entries(
 | 
			
		||||
        data?.lifecycleSummary || {},
 | 
			
		||||
    ).reduce(
 | 
			
		||||
        (acc, [key, value]) => {
 | 
			
		||||
            acc[key === 'preLive' ? 'pre-live' : key] = value.currentFlags || 0;
 | 
			
		||||
            return acc;
 | 
			
		||||
        },
 | 
			
		||||
        {} as Record<string, number>,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const isArchivedFilterActive = state.archived?.values?.includes('true');
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (isArchivedFilterActive && state.lifecycle) {
 | 
			
		||||
            onChange({ ...state, lifecycle: null });
 | 
			
		||||
        }
 | 
			
		||||
    }, [isArchivedFilterActive, state, onChange]);
 | 
			
		||||
 | 
			
		||||
    if (isArchivedFilterActive) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
                marginRight: 'auto',
 | 
			
		||||
                margin: isSmallScreen ? theme.spacing(0, 2) : '0 auto 0 0',
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <LifecycleFilters
 | 
			
		||||
                state={state}
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                total={total}
 | 
			
		||||
                countData={lifecycleSummary}
 | 
			
		||||
            >
 | 
			
		||||
                {children}
 | 
			
		||||
            </LifecycleFilters>
 | 
			
		||||
        </Box>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { useEffect, useState, type VFC } from 'react';
 | 
			
		||||
import { useEffect, useState, type FC } from 'react';
 | 
			
		||||
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
 | 
			
		||||
import {
 | 
			
		||||
    type FilterItemParamHolder,
 | 
			
		||||
@ -8,13 +8,13 @@ import {
 | 
			
		||||
import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators';
 | 
			
		||||
import { formatTag } from 'utils/format-tag';
 | 
			
		||||
 | 
			
		||||
interface IProjectOverviewFilters {
 | 
			
		||||
type ProjectOverviewFiltersProps = {
 | 
			
		||||
    state: FilterItemParamHolder;
 | 
			
		||||
    onChange: (value: FilterItemParamHolder) => void;
 | 
			
		||||
    project: string;
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
 | 
			
		||||
export const ProjectOverviewFilters: FC<ProjectOverviewFiltersProps> = ({
 | 
			
		||||
    state,
 | 
			
		||||
    onChange,
 | 
			
		||||
    project,
 | 
			
		||||
@ -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