1
0
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:
Tymoteusz Czech 2023-12-15 11:37:49 +01:00 committed by GitHub
parent 8be09510e8
commit 203d6ac848
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 41 additions and 903 deletions

View File

@ -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',
);
});

View File

@ -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 />;
};

View File

@ -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>

View File

@ -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),

View File

@ -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

View File

@ -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>
);
};

View File

@ -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', {

View File

@ -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,
}),
);

View File

@ -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 />}

View File

@ -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
&ldquo;
{tableState.query}
&rdquo;
</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>
</>
);
};

View File

@ -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}