mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-19 17:52:45 +02: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 { Box, Chip, styled } from '@mui/material';
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { SxProps, Theme } from '@mui/material';
|
||||||
import type { FilterItemParamHolder } from '../../../filter/Filters/Filters.tsx';
|
import type { ReactNode } from 'react';
|
||||||
import type { LifecycleStage } from '../../FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx';
|
import type { FilterItemParamHolder } from '../../filter/Filters/Filters.tsx';
|
||||||
import { useLifecycleCount } from 'hooks/api/getters/useLifecycleCount/useLifecycleCount';
|
import type { LifecycleStage } from '../../feature/FeatureView/FeatureOverview/FeatureLifecycle/LifecycleStage.tsx';
|
||||||
import type { FeatureLifecycleCountSchema } from 'openapi';
|
|
||||||
|
|
||||||
const StyledChip = styled(Chip, {
|
const StyledChip = styled(Chip, {
|
||||||
shouldForwardProp: (prop) => prop !== 'isActive',
|
shouldForwardProp: (prop) => prop !== 'isActive',
|
||||||
@ -26,17 +25,18 @@ const StyledChip = styled(Chip, {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface ILifecycleFiltersProps {
|
interface ILifecycleFiltersBaseProps {
|
||||||
state: FilterItemParamHolder;
|
state: FilterItemParamHolder;
|
||||||
onChange: (value: FilterItemParamHolder) => void;
|
onChange: (value: FilterItemParamHolder) => void;
|
||||||
total?: number;
|
total?: number;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
countData?: Record<LifecycleStage['name'], number>;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = styled(Box)(({ theme }) => ({
|
const Wrapper = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
padding: theme.spacing(1.5, 3, 0, 3),
|
|
||||||
minHeight: theme.spacing(7),
|
minHeight: theme.spacing(7),
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
@ -58,34 +58,13 @@ const lifecycleOptions: {
|
|||||||
{ label: 'Cleanup', value: 'completed' },
|
{ label: 'Cleanup', value: 'completed' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getStageCount = (
|
export const LifecycleFilters = ({
|
||||||
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,
|
state,
|
||||||
onChange,
|
onChange,
|
||||||
total,
|
total,
|
||||||
children,
|
children,
|
||||||
}) => {
|
countData,
|
||||||
const { lifecycleCount } = useLifecycleCount();
|
}: ILifecycleFiltersBaseProps) => {
|
||||||
const current = state.lifecycle?.values ?? [];
|
const current = state.lifecycle?.values ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -96,7 +75,7 @@ export const LifecycleFilters: FC<ILifecycleFiltersProps> = ({
|
|||||||
value === null
|
value === null
|
||||||
? !state.lifecycle
|
? !state.lifecycle
|
||||||
: current.includes(value);
|
: current.includes(value);
|
||||||
const count = getStageCount(value, lifecycleCount);
|
const count = value ? countData?.[value] : total;
|
||||||
const dynamicLabel =
|
const dynamicLabel =
|
||||||
isActive && Number.isInteger(total)
|
isActive && Number.isInteger(total)
|
||||||
? `${label} (${total === count ? total : `${total} of ${count}`})`
|
? `${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 { focusable } from 'themes/themeStyles';
|
||||||
import { FeatureLifecycleCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
import { FeatureLifecycleCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { FeatureToggleFilters } from './FeatureToggleFilters/FeatureToggleFilters.tsx';
|
import { FeaturesOverviewToggleFilters } from './FeaturesOverviewLifecycleFilters/FeaturesOverviewToggleFilters.tsx';
|
||||||
import { withTableState } from 'utils/withTableState';
|
import { withTableState } from 'utils/withTableState';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
@ -29,7 +29,7 @@ import {
|
|||||||
useTableStateFilter,
|
useTableStateFilter,
|
||||||
} from './useGlobalFeatureSearch.ts';
|
} from './useGlobalFeatureSearch.ts';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
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 { ExportFlags } from './ExportFlags.tsx';
|
||||||
import { createFeatureOverviewCell } from 'component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
|
import { createFeatureOverviewCell } from 'component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
|
||||||
import { AvatarCell } from 'component/project/Project/PaginatedProjectFeatureToggles/AvatarCell';
|
import { AvatarCell } from 'component/project/Project/PaginatedProjectFeatureToggles/AvatarCell';
|
||||||
@ -290,7 +290,7 @@ export const FeatureToggleListTable: FC = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LifecycleFilters
|
<FeaturesOverviewLifecycleFilters
|
||||||
state={filterState}
|
state={filterState}
|
||||||
onChange={setTableState}
|
onChange={setTableState}
|
||||||
total={loading ? undefined : total}
|
total={loading ? undefined : total}
|
||||||
@ -303,8 +303,8 @@ export const FeatureToggleListTable: FC = () => {
|
|||||||
id='globalFeatureFlags'
|
id='globalFeatureFlags'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</LifecycleFilters>
|
</FeaturesOverviewLifecycleFilters>
|
||||||
<FeatureToggleFilters
|
<FeaturesOverviewToggleFilters
|
||||||
onChange={setTableState}
|
onChange={setTableState}
|
||||||
state={filterState}
|
state={filterState}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { type MockedFunction, vi } from 'vitest';
|
import { type MockedFunction, vi } from 'vitest';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import userEvent from '@testing-library/user-event';
|
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';
|
import { useLifecycleCount } from 'hooks/api/getters/useLifecycleCount/useLifecycleCount';
|
||||||
|
|
||||||
vi.mock('hooks/api/getters/useLifecycleCount/useLifecycleCount');
|
vi.mock('hooks/api/getters/useLifecycleCount/useLifecycleCount');
|
||||||
@ -33,7 +33,7 @@ describe('LifecycleFilters', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<LifecycleFilters state={{}} onChange={vi.fn()} />,
|
<FeaturesOverviewLifecycleFilters state={{}} onChange={vi.fn()} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText('All flags')).toBeInTheDocument();
|
expect(getByText('All flags')).toBeInTheDocument();
|
||||||
@ -44,10 +44,10 @@ describe('LifecycleFilters', () => {
|
|||||||
|
|
||||||
it('renders all stages with correct counts when no total provided', () => {
|
it('renders all stages with correct counts when no total provided', () => {
|
||||||
const { getByText } = render(
|
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('Develop (2)')).toBeInTheDocument();
|
||||||
expect(getByText('Rollout production (3)')).toBeInTheDocument();
|
expect(getByText('Rollout production (3)')).toBeInTheDocument();
|
||||||
expect(getByText('Cleanup (4)')).toBeInTheDocument();
|
expect(getByText('Cleanup (4)')).toBeInTheDocument();
|
||||||
@ -56,7 +56,7 @@ describe('LifecycleFilters', () => {
|
|||||||
it('renders dynamic label when total matches count', () => {
|
it('renders dynamic label when total matches count', () => {
|
||||||
const total = 3;
|
const total = 3;
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<LifecycleFilters
|
<FeaturesOverviewLifecycleFilters
|
||||||
state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
|
state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
|
||||||
onChange={vi.fn()}
|
onChange={vi.fn()}
|
||||||
total={total}
|
total={total}
|
||||||
@ -68,7 +68,7 @@ describe('LifecycleFilters', () => {
|
|||||||
it('renders dynamic label when total does not match count', () => {
|
it('renders dynamic label when total does not match count', () => {
|
||||||
const total = 2;
|
const total = 2;
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<LifecycleFilters
|
<FeaturesOverviewLifecycleFilters
|
||||||
state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
|
state={{ lifecycle: { operator: 'IS', values: ['live'] } }}
|
||||||
onChange={vi.fn()}
|
onChange={vi.fn()}
|
||||||
total={total}
|
total={total}
|
||||||
@ -80,7 +80,7 @@ describe('LifecycleFilters', () => {
|
|||||||
it('will apply a correct filter for each stage', async () => {
|
it('will apply a correct filter for each stage', async () => {
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<LifecycleFilters state={{}} onChange={onChange} />,
|
<FeaturesOverviewLifecycleFilters state={{}} onChange={onChange} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
await userEvent.click(getByText('Develop (2)'));
|
await userEvent.click(getByText('Develop (2)'));
|
||||||
@ -98,7 +98,7 @@ describe('LifecycleFilters', () => {
|
|||||||
lifecycle: { operator: 'IS', values: ['completed'] },
|
lifecycle: { operator: 'IS', values: ['completed'] },
|
||||||
});
|
});
|
||||||
|
|
||||||
await userEvent.click(getByText('All flags (10)'));
|
await userEvent.click(getByText('All flags'));
|
||||||
expect(onChange).toHaveBeenCalledWith({ lifecycle: null });
|
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 { screen } from '@testing-library/react';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
import { FeatureToggleFilters } from './FeatureToggleFilters.tsx';
|
import { FeaturesOverviewToggleFilters } from './FeaturesOverviewToggleFilters.tsx';
|
||||||
|
|
||||||
const server = testServerSetup();
|
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');
|
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();
|
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 useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
|
import useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
|
||||||
@ -9,15 +9,14 @@ import {
|
|||||||
} from 'component/filter/Filters/Filters';
|
} from 'component/filter/Filters/Filters';
|
||||||
import { formatTag } from 'utils/format-tag';
|
import { formatTag } from 'utils/format-tag';
|
||||||
|
|
||||||
interface IFeatureToggleFiltersProps {
|
type FeaturesOverviewToggleFiltersProps = {
|
||||||
state: FilterItemParamHolder;
|
state: FilterItemParamHolder;
|
||||||
onChange: (value: FilterItemParamHolder) => void;
|
onChange: (value: FilterItemParamHolder) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const FeatureToggleFilters: VFC<IFeatureToggleFiltersProps> = ({
|
export const FeaturesOverviewToggleFilters: FC<
|
||||||
state,
|
FeaturesOverviewToggleFiltersProps
|
||||||
onChange,
|
> = ({ state, onChange }) => {
|
||||||
}) => {
|
|
||||||
const { projects } = useProjects();
|
const { projects } = useProjects();
|
||||||
const { segments } = useSegments();
|
const { segments } = useSegments();
|
||||||
const { tags } = useAllTags();
|
const { tags } = useAllTags();
|
@ -37,6 +37,30 @@ const setupApi = () => {
|
|||||||
{ id: 1, name: 'AuthorA' },
|
{ id: 1, name: 'AuthorA' },
|
||||||
{ id: 2, name: 'AuthorB' },
|
{ 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 () => {
|
test('filters by flag type', async () => {
|
||||||
@ -208,7 +232,7 @@ test('Project is not onboarded', async () => {
|
|||||||
await screen.findByText('Welcome to your project');
|
await screen.findByText('Welcome to your project');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders lifecycle filters', async () => {
|
test('renders lifecycle quick filters', async () => {
|
||||||
setupApi();
|
setupApi();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@ -227,14 +251,8 @@ test('renders lifecycle filters', async () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const addFilter = await screen.findByText('Filter');
|
await screen.findByText(/All flags/);
|
||||||
fireEvent.click(addFilter);
|
await screen.findByText(/Develop/);
|
||||||
|
await screen.findByText(/Rollout production/);
|
||||||
const lifecycleFilter = await screen.findByText('Lifecycle stage');
|
await screen.findByText(/Cleanup/);
|
||||||
fireEvent.click(lifecycleFilter);
|
|
||||||
|
|
||||||
await screen.findByText('Define');
|
|
||||||
await screen.findByText('Develop');
|
|
||||||
await screen.findByText('Rollout production');
|
|
||||||
await screen.findByText('Cleanup');
|
|
||||||
});
|
});
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
PlaceholderFeatureToggleCell,
|
PlaceholderFeatureToggleCell,
|
||||||
} from './FeatureToggleCell/FeatureToggleCell.tsx';
|
} from './FeatureToggleCell/FeatureToggleCell.tsx';
|
||||||
import { ProjectOverviewFilters } from './ProjectOverviewFilters.tsx';
|
import { ProjectOverviewFilters } from './ProjectOverviewFilters.tsx';
|
||||||
|
import { ProjectLifecycleFilters } from './ProjectLifecycleFilters.tsx';
|
||||||
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility.ts';
|
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility.ts';
|
||||||
import { TableEmptyState } from './TableEmptyState/TableEmptyState.tsx';
|
import { TableEmptyState } from './TableEmptyState/TableEmptyState.tsx';
|
||||||
import { useRowActions } from './hooks/useRowActions.tsx';
|
import { useRowActions } from './hooks/useRowActions.tsx';
|
||||||
@ -56,9 +57,9 @@ import { ImportModal } from '../Import/ImportModal.tsx';
|
|||||||
import { IMPORT_BUTTON } from 'utils/testIds';
|
import { IMPORT_BUTTON } from 'utils/testIds';
|
||||||
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
|
import { ProjectCleanupReminder } from './ProjectCleanupReminder/ProjectCleanupReminder.tsx';
|
||||||
|
|
||||||
interface IPaginatedProjectFeatureTogglesProps {
|
type ProjectFeatureTogglesProps = {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
const formatEnvironmentColumnId = (environment: string) =>
|
const formatEnvironmentColumnId = (environment: string) =>
|
||||||
`environment:${environment}`;
|
`environment:${environment}`;
|
||||||
@ -75,7 +76,6 @@ const Container = styled('div')(({ theme }) => ({
|
|||||||
const FilterRow = styled('div')(({ theme }) => ({
|
const FilterRow = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexFlow: 'row wrap',
|
flexFlow: 'row wrap',
|
||||||
gap: theme.spacing(2),
|
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ const ButtonGroup = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
export const ProjectFeatureToggles = ({
|
export const ProjectFeatureToggles = ({
|
||||||
environments,
|
environments,
|
||||||
}: IPaginatedProjectFeatureTogglesProps) => {
|
}: ProjectFeatureTogglesProps) => {
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { project } = useProjectOverview(projectId);
|
const { project } = useProjectOverview(projectId);
|
||||||
@ -577,6 +577,12 @@ export const ProjectFeatureToggles = ({
|
|||||||
onChange={setTableState}
|
onChange={setTableState}
|
||||||
state={filterState}
|
state={filterState}
|
||||||
/>
|
/>
|
||||||
|
<ProjectLifecycleFilters
|
||||||
|
projectId={projectId}
|
||||||
|
state={filterState}
|
||||||
|
onChange={setTableState}
|
||||||
|
total={loading ? undefined : total}
|
||||||
|
/>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
permission={UPDATE_FEATURE}
|
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 useAllTags from 'hooks/api/getters/useAllTags/useAllTags';
|
||||||
import {
|
import {
|
||||||
type FilterItemParamHolder,
|
type FilterItemParamHolder,
|
||||||
@ -8,13 +8,13 @@ import {
|
|||||||
import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators';
|
import { useProjectFlagCreators } from 'hooks/api/getters/useProjectFlagCreators/useProjectFlagCreators';
|
||||||
import { formatTag } from 'utils/format-tag';
|
import { formatTag } from 'utils/format-tag';
|
||||||
|
|
||||||
interface IProjectOverviewFilters {
|
type ProjectOverviewFiltersProps = {
|
||||||
state: FilterItemParamHolder;
|
state: FilterItemParamHolder;
|
||||||
onChange: (value: FilterItemParamHolder) => void;
|
onChange: (value: FilterItemParamHolder) => void;
|
||||||
project: string;
|
project: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
|
export const ProjectOverviewFilters: FC<ProjectOverviewFiltersProps> = ({
|
||||||
state,
|
state,
|
||||||
onChange,
|
onChange,
|
||||||
project,
|
project,
|
||||||
@ -118,19 +118,6 @@ export const ProjectOverviewFilters: VFC<IProjectOverviewFilters> = ({
|
|||||||
singularOperators: ['IS'],
|
singularOperators: ['IS'],
|
||||||
pluralOperators: ['IS_ANY_OF'],
|
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);
|
setAvailableFilters(availableFilters);
|
||||||
|
Loading…
Reference in New Issue
Block a user