mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
refactor: paginated project table cleanup (#5646)
- added `getRowId` - fix row selection - move and rename paginated table
This commit is contained in:
parent
8be09510e8
commit
203d6ac848
@ -34,7 +34,11 @@ const setupNoFeaturesReturned = () =>
|
||||
});
|
||||
|
||||
const setupApi = (features: APIFeature[], projects: APIProject[]) => {
|
||||
testServerRoute(server, '/api/admin/ui-config', {});
|
||||
testServerRoute(server, '/api/admin/ui-config', {
|
||||
flags: {
|
||||
featureSearchFrontend: true,
|
||||
},
|
||||
});
|
||||
|
||||
testServerRoute(server, '/api/admin/projects', {
|
||||
projects,
|
||||
@ -75,12 +79,12 @@ const setupApi = (features: APIFeature[], projects: APIProject[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
const verifyTableFeature = async (feature: UIFeature) => {
|
||||
const verifyTableFeature = async (feature: Partial<UIFeature>) => {
|
||||
await screen.findByText('Feature toggles');
|
||||
await screen.findByText('Add Filter');
|
||||
Object.values(feature).forEach((value) => {
|
||||
expect(screen.getByText(value)).toBeInTheDocument();
|
||||
});
|
||||
await Promise.all(
|
||||
Object.values(feature).map((value) => screen.findByText(value)),
|
||||
);
|
||||
};
|
||||
|
||||
const filterFeaturesByProject = async (projectName: string) => {
|
||||
@ -137,6 +141,6 @@ test('Filter table by project', async () => {
|
||||
'No feature toggles available. Get started by adding a new feature toggle.',
|
||||
);
|
||||
expect(window.location.href).toContain(
|
||||
'?offset=0&columns=&project=IS%3Aproject-b',
|
||||
'?sort=createdAt&order=desc&offset=0&columns=&project=IS%3Aproject-b',
|
||||
);
|
||||
});
|
||||
|
@ -51,6 +51,8 @@ import { withTableState } from 'utils/withTableState';
|
||||
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||
import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentCell/FeatureSegmentCell';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { FeatureToggleListTable as LegacyFeatureToggleListTable } from './LegacyFeatureToggleListTable';
|
||||
|
||||
export const featuresPlaceholder = Array(15).fill({
|
||||
name: 'Name of the feature',
|
||||
@ -62,7 +64,7 @@ export const featuresPlaceholder = Array(15).fill({
|
||||
|
||||
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
||||
|
||||
export const FeatureToggleListTable: VFC = () => {
|
||||
const FeatureToggleListTableComponent: VFC = () => {
|
||||
const theme = useTheme();
|
||||
const { environments } = useEnvironments();
|
||||
const enabledEnvironments = environments
|
||||
@ -379,3 +381,11 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeatureToggleListTable: VFC = () => {
|
||||
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
||||
|
||||
if (featureSearchFrontend) return <FeatureToggleListTableComponent />;
|
||||
|
||||
return <LegacyFeatureToggleListTable />;
|
||||
};
|
||||
|
@ -115,7 +115,6 @@ const StyledIconButton = styled(IconButton)<{
|
||||
}));
|
||||
|
||||
const Header: VFC = () => {
|
||||
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
||||
const { onSetThemeMode, themeMode } = useThemeMode();
|
||||
const theme = useTheme();
|
||||
const adminId = useId();
|
||||
@ -192,13 +191,7 @@ const Header: VFC = () => {
|
||||
<StyledNav>
|
||||
<StyledLinks>
|
||||
<StyledLink to='/projects'>Projects</StyledLink>
|
||||
<StyledLink
|
||||
to={
|
||||
featureSearchFrontend
|
||||
? '/features-new'
|
||||
: '/features'
|
||||
}
|
||||
>
|
||||
<StyledLink to={'/features'}>
|
||||
Feature toggles
|
||||
</StyledLink>
|
||||
<StyledLink to='/playground'>Playground</StyledLink>
|
||||
|
@ -123,14 +123,6 @@ exports[`returns all baseRoutes 1`] = `
|
||||
"title": "Feature toggles",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": [Function],
|
||||
"flag": "featureSearchFrontend",
|
||||
"menu": {},
|
||||
"path": "/features-new",
|
||||
"title": "Feature toggles",
|
||||
"type": "protected",
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"$$typeof": Symbol(react.lazy),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { FeatureToggleListTable } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
|
||||
import { FeatureToggleListTable as LegacyFeatureToggleListTable } from 'component/feature/FeatureToggleList/LegacyFeatureToggleListTable';
|
||||
import { StrategyView } from 'component/strategies/StrategyView/StrategyView';
|
||||
import { StrategiesList } from 'component/strategies/StrategiesList/StrategiesList';
|
||||
import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
|
||||
@ -145,17 +144,9 @@ export const routes: IRoute[] = [
|
||||
{
|
||||
path: '/features',
|
||||
title: 'Feature toggles',
|
||||
component: LegacyFeatureToggleListTable,
|
||||
type: 'protected',
|
||||
menu: { mobile: true },
|
||||
},
|
||||
{
|
||||
path: '/features-new',
|
||||
title: 'Feature toggles',
|
||||
component: FeatureToggleListTable,
|
||||
type: 'protected',
|
||||
menu: {}, // TODO: Add mobile menu when removing `featureSearchFrontend` flag
|
||||
flag: 'featureSearchFrontend',
|
||||
menu: { mobile: true },
|
||||
},
|
||||
|
||||
// Playground
|
||||
|
@ -1,93 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import useProject, {
|
||||
useProjectNameOrId,
|
||||
} from 'hooks/api/getters/useProject/useProject';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { ProjectFeatureToggles } from '../ProjectFeatureToggles/ProjectFeatureToggles';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ExperimentalProjectFeatureToggles } from './ExperimentalProjectTable/ExperimentalProjectTable';
|
||||
|
||||
const refreshInterval = 15 * 1000;
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledProjectToggles = styled('div')(() => ({
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
}));
|
||||
|
||||
const StyledContentContainer = styled(Box)(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
}));
|
||||
|
||||
const PaginatedProjectOverview = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { project, loading: projectLoading } = useProject(projectId, {
|
||||
refreshInterval,
|
||||
});
|
||||
|
||||
const { environments } = project;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContentContainer>
|
||||
<StyledProjectToggles>
|
||||
<ExperimentalProjectFeatureToggles
|
||||
environments={environments}
|
||||
storageKey='project-features'
|
||||
/>
|
||||
</StyledProjectToggles>
|
||||
</StyledContentContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated remove when flag `featureSearchFrontend` is removed
|
||||
*/
|
||||
export const ExperimentalProjectFeatures = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const projectName = useProjectNameOrId(projectId);
|
||||
const { project, loading, refetch } = useProject(projectId, {
|
||||
refreshInterval,
|
||||
});
|
||||
const { features, environments } = project;
|
||||
usePageTitle(`Project overview – ${projectName}`);
|
||||
const { setLastViewed } = useLastViewedProject();
|
||||
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
||||
|
||||
useEffect(() => {
|
||||
setLastViewed(projectId);
|
||||
}, [projectId, setLastViewed]);
|
||||
|
||||
if (featureSearchFrontend) return <PaginatedProjectOverview />;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContentContainer>
|
||||
<StyledProjectToggles>
|
||||
<ProjectFeatureToggles
|
||||
style={{ width: '100%', margin: 0 }}
|
||||
key={loading ? 'loading' : 'ready'}
|
||||
features={features}
|
||||
environments={environments}
|
||||
loading={loading}
|
||||
onChange={refetch}
|
||||
/>
|
||||
</StyledProjectToggles>
|
||||
</StyledContentContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -3,8 +3,8 @@ import { styled } from '@mui/material';
|
||||
import { flexRow } from 'themes/themeStyles';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
|
||||
import { FeatureToggleSwitch } from '../../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
||||
import type { UseFeatureToggleSwitchType } from '../../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.types';
|
||||
import { FeatureToggleSwitch } from '../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
||||
import type { UseFeatureToggleSwitchType } from '../../ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.types';
|
||||
import { type FeatureEnvironmentSchema } from 'openapi';
|
||||
|
||||
const StyledSwitchContainer = styled('div', {
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
@ -9,17 +9,17 @@ import { PaginatedTable } from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||
import { ProjectEnvironmentType } from '../../ProjectFeatureToggles/hooks/useEnvironmentsRef';
|
||||
import { ActionsCell } from '../../ProjectFeatureToggles/ActionsCell/ActionsCell';
|
||||
import { ProjectEnvironmentType } from '../ProjectFeatureToggles/hooks/useEnvironmentsRef';
|
||||
import { ActionsCell } from '../ProjectFeatureToggles/ActionsCell/ActionsCell';
|
||||
import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||
import { MemoizedRowSelectCell } from '../../ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
||||
import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
||||
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||
import { ProjectFeaturesBatchActions } from '../../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||
import { ProjectFeaturesBatchActions } from '../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||
import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { useFeatureToggleSwitch } from '../../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||
import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||
import useLoading from 'hooks/useLoading';
|
||||
import {
|
||||
DEFAULT_PAGE_LIMIT,
|
||||
@ -32,11 +32,11 @@ import {
|
||||
FilterItemParam,
|
||||
} from 'utils/serializeQueryParams';
|
||||
import {
|
||||
ArrayParam,
|
||||
encodeQueryParams,
|
||||
NumberParam,
|
||||
StringParam,
|
||||
ArrayParam,
|
||||
withDefault,
|
||||
encodeQueryParams,
|
||||
} from 'use-query-params';
|
||||
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
|
||||
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
||||
@ -50,7 +50,7 @@ import { Placeholder } from './TablePlaceholder/TablePlaceholder';
|
||||
import { useRowActions } from './hooks/useRowActions';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
interface IExperimentalProjectFeatureTogglesProps {
|
||||
interface IPaginatedProjectFeatureTogglesProps {
|
||||
environments: IProject['environments'];
|
||||
refreshInterval?: number;
|
||||
storageKey?: string;
|
||||
@ -60,12 +60,13 @@ const formatEnvironmentColumnId = (environment: string) =>
|
||||
`environment:${environment}`;
|
||||
|
||||
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
||||
const getRowId = (row: { name: string }) => row.name;
|
||||
|
||||
export const ExperimentalProjectFeatureToggles = ({
|
||||
export const PaginatedProjectFeatureToggles = ({
|
||||
environments,
|
||||
refreshInterval = 15 * 1000,
|
||||
storageKey = 'project-feature-toggles',
|
||||
}: IExperimentalProjectFeatureTogglesProps) => {
|
||||
}: IPaginatedProjectFeatureTogglesProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
|
||||
const featuresExportImport = useUiFlag('featuresExportImport');
|
||||
@ -338,6 +339,7 @@ export const ExperimentalProjectFeatureToggles = ({
|
||||
state: {
|
||||
columnVisibility: defaultColumnVisibility,
|
||||
},
|
||||
getRowId,
|
||||
}),
|
||||
);
|
||||
|
@ -40,7 +40,6 @@ import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadg
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { ProjectDoraMetrics } from './ProjectDoraMetrics/ProjectDoraMetrics';
|
||||
import { UiFlags } from 'interfaces/uiConfig';
|
||||
import { ExperimentalProjectFeatures } from './ExperimentalProjectFeatures/ExperimentalProjectFeatures';
|
||||
import { HiddenProjectIconWithTooltip } from './HiddenProjectIconWithTooltip/HiddenProjectIconWithTooltip';
|
||||
|
||||
const StyledBadge = styled(Badge)(({ theme }) => ({
|
||||
@ -288,10 +287,6 @@ export const Project = () => {
|
||||
<Route path='environments' element={<ProjectEnvironment />} />
|
||||
<Route path='archive' element={<ProjectFeaturesArchive />} />
|
||||
<Route path='logs' element={<ProjectLog />} />
|
||||
<Route
|
||||
path='features'
|
||||
element={<ExperimentalProjectFeatures />}
|
||||
/>
|
||||
<Route
|
||||
path='change-requests'
|
||||
element={<ProjectChangeRequests />}
|
||||
|
@ -1,751 +0,0 @@
|
||||
import React, {
|
||||
type CSSProperties,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Checkbox,
|
||||
IconButton,
|
||||
styled,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
Box,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
useFlexLayout,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||
import { getCreateTogglePath } from 'utils/routePathHelpers';
|
||||
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||
import { IProject } from 'interfaces/project';
|
||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
||||
import { Search } from 'component/common/Search/Search';
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||
import { ProjectEnvironmentType } from './hooks/useEnvironmentsRef';
|
||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||
import FileDownload from '@mui/icons-material/FileDownload';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||
import { MemoizedRowSelectCell } from './RowSelectCell/RowSelectCell';
|
||||
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||
import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { ListItemType } from './ProjectFeatureToggles.types';
|
||||
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
||||
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||
import useLoading from 'hooks/useLoading';
|
||||
import { StickyPaginationBar } from '../../../common/Table/StickyPaginationBar/StickyPaginationBar';
|
||||
import {
|
||||
DEFAULT_PAGE_LIMIT,
|
||||
useFeatureSearch,
|
||||
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||
import mapValues from 'lodash.mapvalues';
|
||||
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
||||
import { BooleansStringParam } from 'utils/serializeQueryParams';
|
||||
import {
|
||||
NumberParam,
|
||||
StringParam,
|
||||
ArrayParam,
|
||||
withDefault,
|
||||
} from 'use-query-params';
|
||||
|
||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||
whiteSpace: 'nowrap',
|
||||
}));
|
||||
|
||||
interface IPaginatedProjectFeatureTogglesProps {
|
||||
environments: IProject['environments'];
|
||||
style?: CSSProperties;
|
||||
refreshInterval?: number;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||
|
||||
export const PaginatedProjectFeatureToggles = ({
|
||||
environments,
|
||||
style,
|
||||
refreshInterval = 15 * 1000,
|
||||
storageKey = 'project-feature-toggles',
|
||||
}: IPaginatedProjectFeatureTogglesProps) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const [tableState, setTableState] = usePersistentTableState(
|
||||
`${storageKey}-${projectId}`,
|
||||
{
|
||||
offset: withDefault(NumberParam, 0),
|
||||
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
|
||||
query: StringParam,
|
||||
favoritesFirst: withDefault(BooleansStringParam, true),
|
||||
sortBy: withDefault(StringParam, 'createdAt'),
|
||||
sortOrder: withDefault(StringParam, 'desc'),
|
||||
columns: ArrayParam,
|
||||
},
|
||||
);
|
||||
|
||||
const { features, total, refetch, loading, initialLoad } = useFeatureSearch(
|
||||
mapValues({ ...tableState, project: `IS:${projectId}` }, (value) =>
|
||||
value ? `${value}` : undefined,
|
||||
),
|
||||
{
|
||||
refreshInterval,
|
||||
},
|
||||
);
|
||||
const onChange = refetch;
|
||||
|
||||
const { classes: styles } = useStyles();
|
||||
const bodyLoadingRef = useLoading(loading);
|
||||
const headerLoadingRef = useLoading(initialLoad);
|
||||
const theme = useTheme();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
|
||||
featureId?: string;
|
||||
stale?: boolean;
|
||||
}>({});
|
||||
const [featureArchiveState, setFeatureArchiveState] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [isCustomColumns, setIsCustomColumns] = useState(
|
||||
Boolean(tableState.columns),
|
||||
);
|
||||
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
||||
useFeatureToggleSwitch(projectId);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const onFavorite = useCallback(
|
||||
async (feature: IFeatureToggleListItem) => {
|
||||
if (feature?.favorite) {
|
||||
await unfavorite(projectId, feature.name);
|
||||
} else {
|
||||
await favorite(projectId, feature.name);
|
||||
}
|
||||
onChange();
|
||||
},
|
||||
[projectId, onChange],
|
||||
);
|
||||
|
||||
const showTagsColumn = useMemo(
|
||||
() => features.some((feature) => feature?.tags?.length),
|
||||
[features],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'Select',
|
||||
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
||||
<Checkbox {...getToggleAllRowsSelectedProps()} />
|
||||
),
|
||||
Cell: ({ row }: any) => (
|
||||
<MemoizedRowSelectCell
|
||||
{...row?.getToggleRowSelectedProps?.()}
|
||||
/>
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
styles: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'favorite',
|
||||
Header: (
|
||||
<FavoriteIconHeader
|
||||
isActive={tableState.favoritesFirst}
|
||||
onClick={() =>
|
||||
setTableState({
|
||||
favoritesFirst: !tableState.favoritesFirst,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
accessor: 'favorite',
|
||||
Cell: ({ row: { original: feature } }: any) => (
|
||||
<FavoriteIconCell
|
||||
value={feature?.favorite}
|
||||
onClick={() => onFavorite(feature)}
|
||||
/>
|
||||
),
|
||||
maxWidth: 50,
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
},
|
||||
{
|
||||
Header: 'Seen',
|
||||
accessor: 'lastSeenAt',
|
||||
Cell: ({ value, row: { original: feature } }: any) => {
|
||||
return (
|
||||
<MemoizedFeatureEnvironmentSeenCell
|
||||
feature={feature}
|
||||
data-loading
|
||||
/>
|
||||
);
|
||||
},
|
||||
align: 'center',
|
||||
maxWidth: 80,
|
||||
},
|
||||
{
|
||||
Header: 'Type',
|
||||
accessor: 'type',
|
||||
Cell: FeatureTypeCell,
|
||||
align: 'center',
|
||||
filterName: 'type',
|
||||
maxWidth: 80,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
Cell: ({
|
||||
value,
|
||||
}: {
|
||||
value: string;
|
||||
}) => (
|
||||
<Tooltip title={value} arrow describeChild>
|
||||
<span>
|
||||
<LinkCell
|
||||
title={value}
|
||||
to={`/projects/${projectId}/features/${value}`}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
minWidth: 100,
|
||||
sortType: 'alphanumeric',
|
||||
searchable: true,
|
||||
},
|
||||
...(showTagsColumn
|
||||
? [
|
||||
{
|
||||
id: 'tags',
|
||||
Header: 'Tags',
|
||||
accessor: (row: IFeatureToggleListItem) =>
|
||||
row.tags
|
||||
?.map(({ type, value }) => `${type}:${value}`)
|
||||
.join('\n') || '',
|
||||
Cell: FeatureTagCell,
|
||||
width: 80,
|
||||
searchable: true,
|
||||
filterName: 'tags',
|
||||
filterBy(
|
||||
row: IFeatureToggleListItem,
|
||||
values: string[],
|
||||
) {
|
||||
return includesFilter(
|
||||
getColumnValues(this, row),
|
||||
values,
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
Header: 'Created',
|
||||
accessor: 'createdAt',
|
||||
Cell: DateCell,
|
||||
minWidth: 120,
|
||||
},
|
||||
...environments.map(
|
||||
(projectEnvironment: ProjectEnvironmentType | string) => {
|
||||
const name =
|
||||
typeof projectEnvironment === 'string'
|
||||
? projectEnvironment
|
||||
: (projectEnvironment as ProjectEnvironmentType)
|
||||
.environment;
|
||||
const isChangeRequestEnabled =
|
||||
isChangeRequestConfigured(name);
|
||||
const FeatureToggleCell = createFeatureToggleCell(
|
||||
projectId,
|
||||
name,
|
||||
isChangeRequestEnabled,
|
||||
onChange,
|
||||
onFeatureToggle,
|
||||
);
|
||||
|
||||
return {
|
||||
Header: loading ? () => '' : name,
|
||||
maxWidth: 90,
|
||||
id: `environment:${name}`,
|
||||
accessor: (row: ListItemType) => {
|
||||
return row.environments?.[name]?.enabled;
|
||||
},
|
||||
align: 'center',
|
||||
Cell: FeatureToggleCell,
|
||||
sortType: 'boolean',
|
||||
sortDescFirst: true,
|
||||
filterName: name,
|
||||
filterParsing: (value: boolean) =>
|
||||
value ? 'enabled' : 'disabled',
|
||||
};
|
||||
},
|
||||
),
|
||||
{
|
||||
id: 'Actions',
|
||||
maxWidth: 56,
|
||||
width: 56,
|
||||
Cell: (props: {
|
||||
row: {
|
||||
original: ListItemType;
|
||||
};
|
||||
}) => (
|
||||
<ActionsCell
|
||||
projectId={projectId}
|
||||
onOpenArchiveDialog={setFeatureArchiveState}
|
||||
onOpenStaleDialog={setFeatureStaleDialogState}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
disableSortBy: true,
|
||||
hideInMenu: true,
|
||||
styles: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
[projectId, environments, loading, tableState.favoritesFirst, onChange],
|
||||
);
|
||||
|
||||
const [showTitle, setShowTitle] = useState(true);
|
||||
|
||||
const featuresData = useMemo(
|
||||
() =>
|
||||
features.map((feature) => ({
|
||||
...feature,
|
||||
environments: Object.fromEntries(
|
||||
environments.map((env) => {
|
||||
const thisEnv = feature?.environments?.find(
|
||||
(featureEnvironment) =>
|
||||
featureEnvironment?.name === env.environment,
|
||||
);
|
||||
return [
|
||||
env.environment,
|
||||
{
|
||||
name: env.environment,
|
||||
enabled: thisEnv?.enabled || false,
|
||||
variantCount: thisEnv?.variantCount || 0,
|
||||
lastSeenAt: thisEnv?.lastSeenAt,
|
||||
type: thisEnv?.type,
|
||||
hasStrategies: thisEnv?.hasStrategies,
|
||||
hasEnabledStrategies:
|
||||
thisEnv?.hasEnabledStrategies,
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
someEnabledEnvironmentHasVariants:
|
||||
feature.environments?.some(
|
||||
(featureEnvironment) =>
|
||||
featureEnvironment.variantCount &&
|
||||
featureEnvironment.variantCount > 0 &&
|
||||
featureEnvironment.enabled,
|
||||
) || false,
|
||||
})),
|
||||
[features, environments],
|
||||
);
|
||||
|
||||
const { getSearchText, getSearchContext } = useSearch(
|
||||
columns,
|
||||
tableState.query || '',
|
||||
featuresData,
|
||||
);
|
||||
|
||||
const allColumnIds = columns
|
||||
.map(
|
||||
(column: any) =>
|
||||
(column?.id as string) ||
|
||||
(typeof column?.accessor === 'string'
|
||||
? (column?.accessor as string)
|
||||
: ''),
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const initialState = useMemo(
|
||||
() => ({
|
||||
sortBy: [
|
||||
{
|
||||
id: tableState.sortBy || 'createdAt',
|
||||
desc: tableState.sortOrder === 'desc',
|
||||
},
|
||||
],
|
||||
...(tableState.columns
|
||||
? {
|
||||
hiddenColumns: allColumnIds.filter(
|
||||
(id) =>
|
||||
!tableState.columns?.includes(id) &&
|
||||
!staticColumns.includes(id),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
pageSize: tableState.limit,
|
||||
pageIndex: tableState.offset * tableState.limit,
|
||||
selectedRowIds: {},
|
||||
}),
|
||||
[initialLoad],
|
||||
);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (initialLoad || loading) {
|
||||
const loadingData = Array(tableState.limit)
|
||||
.fill(null)
|
||||
.map((_, index) => ({
|
||||
id: index, // Assuming `id` is a required property
|
||||
type: '-',
|
||||
name: `Feature name ${index}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
environments: [
|
||||
{
|
||||
name: 'production',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
// Coerce loading data to FeatureSchema[]
|
||||
return loadingData as unknown as typeof featuresData;
|
||||
}
|
||||
return featuresData;
|
||||
}, [loading, featuresData]);
|
||||
|
||||
const pageCount = useMemo(
|
||||
() => Math.ceil((total || 0) / tableState.limit),
|
||||
[total, tableState.limit],
|
||||
);
|
||||
const getRowId = useCallback((row: any) => row.name, []);
|
||||
|
||||
const {
|
||||
allColumns,
|
||||
headerGroups,
|
||||
rows,
|
||||
state: { pageIndex, pageSize, hiddenColumns, selectedRowIds, sortBy },
|
||||
canNextPage,
|
||||
canPreviousPage,
|
||||
previousPage,
|
||||
nextPage,
|
||||
setPageSize,
|
||||
prepareRow,
|
||||
setHiddenColumns,
|
||||
toggleAllRowsSelected,
|
||||
} = useTable(
|
||||
{
|
||||
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||
data,
|
||||
initialState,
|
||||
autoResetHiddenColumns: false,
|
||||
autoResetSelectedRows: false,
|
||||
disableSortRemove: true,
|
||||
autoResetSortBy: false,
|
||||
manualSortBy: true,
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
getRowId,
|
||||
},
|
||||
useFlexLayout,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
);
|
||||
|
||||
// Refetching - https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/faq.md#how-can-i-use-the-table-state-to-fetch-new-data
|
||||
useEffect(() => {
|
||||
setTableState({
|
||||
offset: pageIndex * pageSize,
|
||||
limit: pageSize,
|
||||
sortBy: sortBy[0]?.id || 'createdAt',
|
||||
sortOrder: sortBy[0]?.desc ? 'desc' : 'asc',
|
||||
});
|
||||
}, [pageIndex, pageSize, sortBy]);
|
||||
|
||||
useEffect(() => {
|
||||
// FIXME: refactor column visibility logic when switching to react-table v8
|
||||
if (!loading && isCustomColumns) {
|
||||
setTableState({
|
||||
columns:
|
||||
hiddenColumns !== undefined
|
||||
? allColumnIds.filter(
|
||||
(id) =>
|
||||
!hiddenColumns.includes(id) &&
|
||||
!staticColumns.includes(id),
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}, [loading, isCustomColumns, hiddenColumns]);
|
||||
|
||||
const showPaginationBar = Boolean(total && total > pageSize);
|
||||
const paginatedStyles = showPaginationBar
|
||||
? {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageContent
|
||||
disableLoading
|
||||
disablePadding
|
||||
className={styles.container}
|
||||
style={{ ...paginatedStyles, ...style }}
|
||||
header={
|
||||
<Box
|
||||
ref={headerLoadingRef}
|
||||
aria-busy={initialLoad}
|
||||
aria-live='polite'
|
||||
sx={(theme) => ({
|
||||
padding: `${theme.spacing(2.5)} ${theme.spacing(
|
||||
3.125,
|
||||
)}`,
|
||||
})}
|
||||
>
|
||||
<PageHeader
|
||||
titleElement={
|
||||
showTitle
|
||||
? `Feature toggles (${
|
||||
total || rows.length
|
||||
})`
|
||||
: null
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={!isSmallScreen}
|
||||
show={
|
||||
<Search
|
||||
data-loading
|
||||
placeholder='Search and Filter'
|
||||
expandable
|
||||
initialValue={
|
||||
tableState.query || ''
|
||||
}
|
||||
onChange={(value) => {
|
||||
setTableState({
|
||||
query: value,
|
||||
});
|
||||
}}
|
||||
onFocus={() =>
|
||||
setShowTitle(false)
|
||||
}
|
||||
onBlur={() =>
|
||||
setShowTitle(true)
|
||||
}
|
||||
hasFilters
|
||||
getSearchContext={
|
||||
getSearchContext
|
||||
}
|
||||
id='projectFeatureToggles'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ColumnsMenu
|
||||
allColumns={allColumns}
|
||||
staticColumns={staticColumns}
|
||||
dividerAfter={['createdAt']}
|
||||
dividerBefore={['Actions']}
|
||||
isCustomized={isCustomColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
onCustomize={() =>
|
||||
setIsCustomColumns(true)
|
||||
}
|
||||
/>
|
||||
<PageHeader.Divider
|
||||
sx={{ marginLeft: 0 }}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
uiConfig?.flags
|
||||
?.featuresExportImport,
|
||||
)}
|
||||
show={
|
||||
<Tooltip
|
||||
title='Export toggles visible in the table below'
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
data-loading
|
||||
onClick={() =>
|
||||
setShowExportDialog(
|
||||
true,
|
||||
)
|
||||
}
|
||||
sx={(theme) => ({
|
||||
marginRight:
|
||||
theme.spacing(2),
|
||||
})}
|
||||
>
|
||||
<FileDownload />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<StyledResponsiveButton
|
||||
onClick={() =>
|
||||
navigate(
|
||||
getCreateTogglePath(projectId),
|
||||
)
|
||||
}
|
||||
maxWidth='960px'
|
||||
Icon={Add}
|
||||
projectId={projectId}
|
||||
permission={CREATE_FEATURE}
|
||||
data-testid='NAVIGATE_TO_CREATE_FEATURE'
|
||||
>
|
||||
New feature toggle
|
||||
</StyledResponsiveButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={isSmallScreen}
|
||||
show={
|
||||
<Search
|
||||
initialValue={tableState.query || ''}
|
||||
onChange={(value) => {
|
||||
setTableState({ query: value });
|
||||
}}
|
||||
hasFilters
|
||||
getSearchContext={getSearchContext}
|
||||
id='projectFeatureToggles'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageHeader>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={bodyLoadingRef}
|
||||
aria-busy={loading}
|
||||
aria-live='polite'
|
||||
>
|
||||
<SearchHighlightProvider
|
||||
value={getSearchText(tableState.query || '')}
|
||||
>
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
</SearchHighlightProvider>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<ConditionallyRender
|
||||
condition={(tableState.query || '')?.length > 0}
|
||||
show={
|
||||
<Box sx={{ padding: theme.spacing(3) }}>
|
||||
<TablePlaceholder>
|
||||
No feature toggles found matching
|
||||
“
|
||||
{tableState.query}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
</Box>
|
||||
}
|
||||
elseShow={
|
||||
<Box sx={{ padding: theme.spacing(3) }}>
|
||||
<TablePlaceholder>
|
||||
No feature toggles available. Get
|
||||
started by adding a new feature
|
||||
toggle.
|
||||
</TablePlaceholder>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FeatureStaleDialog
|
||||
isStale={featureStaleDialogState.stale === true}
|
||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||
onClose={() => {
|
||||
setFeatureStaleDialogState({});
|
||||
onChange();
|
||||
}}
|
||||
featureId={featureStaleDialogState.featureId || ''}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<FeatureArchiveDialog
|
||||
isOpen={Boolean(featureArchiveState)}
|
||||
onConfirm={onChange}
|
||||
onClose={() => {
|
||||
setFeatureArchiveState(undefined);
|
||||
}}
|
||||
featureIds={[featureArchiveState || '']}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
Boolean(uiConfig?.flags?.featuresExportImport) &&
|
||||
!loading
|
||||
}
|
||||
show={
|
||||
<ExportDialog
|
||||
showExportDialog={showExportDialog}
|
||||
data={data}
|
||||
onClose={() => setShowExportDialog(false)}
|
||||
environments={environments.map(
|
||||
({ environment }) => environment,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{featureToggleModals}
|
||||
</div>
|
||||
</PageContent>
|
||||
<ConditionallyRender
|
||||
condition={showPaginationBar}
|
||||
show={
|
||||
<StickyPaginationBar
|
||||
totalItems={total || 0}
|
||||
pageIndex={pageIndex}
|
||||
fetchNextPage={nextPage}
|
||||
fetchPrevPage={previousPage}
|
||||
pageSize={pageSize}
|
||||
setPageLimit={setPageSize}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<BatchSelectionActionsBar
|
||||
count={Object.keys(selectedRowIds).length}
|
||||
>
|
||||
<ProjectFeaturesBatchActions
|
||||
selectedIds={Object.keys(selectedRowIds)}
|
||||
data={features}
|
||||
projectId={projectId}
|
||||
onResetSelection={() => toggleAllRowsSelected(false)}
|
||||
/>
|
||||
</BatchSelectionActionsBar>
|
||||
</>
|
||||
);
|
||||
};
|
@ -10,10 +10,9 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||
import { ProjectStats } from './ProjectStats/ProjectStats';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { PaginatedProjectFeatureToggles } from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||
|
||||
import { PaginatedProjectFeatureToggles } from './PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||
import { FeatureTypeCount } from '../../../interfaces/project';
|
||||
import { type FeatureTypeCount } from '../../../interfaces/project';
|
||||
|
||||
const refreshInterval = 15 * 1000;
|
||||
|
||||
@ -39,9 +38,8 @@ const StyledContentContainer = styled(Box)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
const PaginatedProjectOverview: FC<{
|
||||
fullWidth?: boolean;
|
||||
storageKey?: string;
|
||||
}> = ({ fullWidth, storageKey = 'project-overview' }) => {
|
||||
}> = ({ storageKey = 'project-overview' }) => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { project } = useProjectOverview(projectId, {
|
||||
refreshInterval,
|
||||
@ -70,9 +68,6 @@ const PaginatedProjectOverview: FC<{
|
||||
<ProjectStats stats={project.stats} />
|
||||
<StyledProjectToggles>
|
||||
<PaginatedProjectFeatureToggles
|
||||
style={
|
||||
fullWidth ? { width: '100%', margin: 0 } : undefined
|
||||
}
|
||||
environments={environments}
|
||||
refreshInterval={refreshInterval}
|
||||
storageKey={storageKey}
|
||||
|
Loading…
Reference in New Issue
Block a user