mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
Project Overview with react-table v8 (#5571)
This commit is contained in:
parent
ba50d1ef69
commit
d11aedc12f
@ -14,6 +14,7 @@ const HeaderCell = <T extends object>(header: Header<T, unknown>) => {
|
|||||||
const column = header.column;
|
const column = header.column;
|
||||||
const isDesc = column.getIsSorted() === 'desc';
|
const isDesc = column.getIsSorted() === 'desc';
|
||||||
const align = column.columnDef.meta?.align || undefined;
|
const align = column.columnDef.meta?.align || undefined;
|
||||||
|
const width = column.columnDef.meta?.width || undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellSortable
|
<CellSortable
|
||||||
@ -22,7 +23,12 @@ const HeaderCell = <T extends object>(header: Header<T, unknown>) => {
|
|||||||
isDescending={isDesc}
|
isDescending={isDesc}
|
||||||
align={align}
|
align={align}
|
||||||
onClick={() => column.toggleSorting()}
|
onClick={() => column.toggleSorting()}
|
||||||
styles={{ borderRadius: '0px' }}
|
styles={{
|
||||||
|
borderRadius: '0px',
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
|
@ -5,11 +5,14 @@ import { getLocalizedDateString } from '../../../util';
|
|||||||
|
|
||||||
interface IDateCellProps {
|
interface IDateCellProps {
|
||||||
value?: Date | string | null;
|
value?: Date | string | null;
|
||||||
|
getValue?: () => Date | string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DateCell: VFC<IDateCellProps> = ({ value }) => {
|
// `getValue is for new @tanstack/react-table (v8), `value` is for legacy react-table (v7)
|
||||||
|
export const DateCell: VFC<IDateCellProps> = ({ value, getValue }) => {
|
||||||
|
const input = value || getValue?.() || null;
|
||||||
const { locationSettings } = useLocationSettings();
|
const { locationSettings } = useLocationSettings();
|
||||||
const date = getLocalizedDateString(value, locationSettings.locale);
|
const date = getLocalizedDateString(input, locationSettings.locale);
|
||||||
|
|
||||||
return <TextCell lineClamp={1}>{date}</TextCell>;
|
return <TextCell lineClamp={1}>{date}</TextCell>;
|
||||||
};
|
};
|
||||||
|
@ -4,17 +4,21 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
|||||||
interface IFeatureNameCellProps {
|
interface IFeatureNameCellProps {
|
||||||
row: {
|
row: {
|
||||||
original: {
|
original: {
|
||||||
name: string;
|
name?: string | null;
|
||||||
description: string;
|
description?: string | null;
|
||||||
project: string;
|
project?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeatureNameCell: VFC<IFeatureNameCellProps> = ({ row }) => (
|
export const FeatureNameCell: VFC<IFeatureNameCellProps> = ({ row }) => (
|
||||||
<LinkCell
|
<LinkCell
|
||||||
title={row.original.name}
|
title={row.original.name || ''}
|
||||||
subtitle={row.original.description}
|
subtitle={row.original.description || ''}
|
||||||
to={`/projects/${row.original.project}/features/${row.original.name}`}
|
to={
|
||||||
|
row.original.project && row.original.name
|
||||||
|
? `/projects/${row.original.project}/features/${row.original.name}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import React, { VFC } from 'react';
|
import React, { VFC } from 'react';
|
||||||
import { FeatureEnvironmentSeen } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
import { FeatureEnvironmentSeen } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
|
||||||
import { FeatureSchema } from 'openapi';
|
import { FeatureEnvironmentSchema } from 'openapi';
|
||||||
|
|
||||||
interface IFeatureSeenCellProps {
|
interface IFeatureSeenCellProps {
|
||||||
feature: FeatureSchema;
|
feature: {
|
||||||
|
environments?: FeatureEnvironmentSchema[];
|
||||||
|
lastSeenAt?: string | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
|
export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({
|
||||||
|
@ -5,6 +5,7 @@ import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
|||||||
|
|
||||||
interface IFeatureTypeProps {
|
interface IFeatureTypeProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
|
getValue?: () => string | undefined | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
@ -15,15 +16,20 @@ const StyledContainer = styled('div')(({ theme }) => ({
|
|||||||
color: theme.palette.text.disabled,
|
color: theme.palette.text.disabled,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const FeatureTypeCell: VFC<IFeatureTypeProps> = ({ value }) => {
|
// `getValue is for new @tanstack/react-table (v8), `value` is for legacy react-table (v7)
|
||||||
|
export const FeatureTypeCell: VFC<IFeatureTypeProps> = ({
|
||||||
|
value,
|
||||||
|
getValue,
|
||||||
|
}) => {
|
||||||
|
const type = value || getValue?.() || undefined;
|
||||||
const { featureTypes } = useFeatureTypes();
|
const { featureTypes } = useFeatureTypes();
|
||||||
const IconComponent = getFeatureTypeIcons(value);
|
const IconComponent = getFeatureTypeIcons(type);
|
||||||
|
|
||||||
const typeName = featureTypes
|
const typeName = featureTypes.find(
|
||||||
.filter((type) => type.id === value)
|
(featureType) => featureType.id === type,
|
||||||
.map((type) => type.name);
|
)?.name;
|
||||||
|
|
||||||
const title = `This is a "${typeName || value}" toggle`;
|
const title = `This is a "${typeName || type}" toggle`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
|
@ -9,7 +9,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|||||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||||
|
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { PaginatedProjectFeatureToggles } from '../ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
import { ExperimentalProjectFeatureToggles } from './ExperimentalProjectTable/ExperimentalProjectTable';
|
||||||
|
|
||||||
const refreshInterval = 15 * 1000;
|
const refreshInterval = 15 * 1000;
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ const PaginatedProjectOverview = () => {
|
|||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<StyledProjectToggles>
|
<StyledProjectToggles>
|
||||||
<PaginatedProjectFeatureToggles
|
<ExperimentalProjectFeatureToggles
|
||||||
style={{ width: '100%', margin: 0 }}
|
style={{ width: '100%', margin: 0 }}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
storageKey='project-features'
|
storageKey='project-features'
|
||||||
|
@ -0,0 +1,428 @@
|
|||||||
|
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 {
|
||||||
|
PaginatedTable,
|
||||||
|
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 '../../ProjectFeatureToggles/hooks/useEnvironmentsRef';
|
||||||
|
import { ActionsCell } from '../../ProjectFeatureToggles/ActionsCell/ActionsCell';
|
||||||
|
import { ColumnsMenu } from '../../ProjectFeatureToggles/ColumnsMenu/ColumnsMenu';
|
||||||
|
import { useStyles } from '../../ProjectFeatureToggles/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 '../../ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
||||||
|
import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||||
|
import { ProjectFeaturesBatchActions } from '../../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
||||||
|
import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { ListItemType } from '../../ProjectFeatureToggles/ProjectFeatureToggles.types';
|
||||||
|
import { createFeatureToggleCell } from '../../ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell';
|
||||||
|
import { useFeatureToggleSwitch } from '../../ProjectFeatureToggles/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';
|
||||||
|
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
|
||||||
|
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
||||||
|
import { withTableState } from 'utils/withTableState';
|
||||||
|
import { type FeatureSearchResponseSchema } from 'openapi';
|
||||||
|
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
||||||
|
import { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell';
|
||||||
|
|
||||||
|
interface IExperimentalProjectFeatureTogglesProps {
|
||||||
|
environments: IProject['environments'];
|
||||||
|
style?: CSSProperties;
|
||||||
|
refreshInterval?: number;
|
||||||
|
storageKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
||||||
|
|
||||||
|
export const ExperimentalProjectFeatureToggles = ({
|
||||||
|
environments,
|
||||||
|
style,
|
||||||
|
refreshInterval = 15 * 1000,
|
||||||
|
storageKey = 'project-feature-toggles',
|
||||||
|
}: IExperimentalProjectFeatureTogglesProps) => {
|
||||||
|
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, projectId }, (value) =>
|
||||||
|
value ? `${value}` : undefined,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
refreshInterval,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||||
|
const onFavorite = useCallback(
|
||||||
|
async (feature: FeatureSearchResponseSchema) => {
|
||||||
|
if (feature?.favorite) {
|
||||||
|
await unfavorite(projectId, feature.name);
|
||||||
|
} else {
|
||||||
|
await favorite(projectId, feature.name);
|
||||||
|
}
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
[projectId, refetch],
|
||||||
|
);
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
||||||
|
useFeatureToggleSwitch(projectId);
|
||||||
|
const bodyLoadingRef = useLoading(loading);
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'Select',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<MemoizedRowSelectCell
|
||||||
|
noPadding
|
||||||
|
title='Select all rows'
|
||||||
|
checked={table?.getIsAllRowsSelected()}
|
||||||
|
onChange={table?.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<MemoizedRowSelectCell
|
||||||
|
noPadding
|
||||||
|
title='Select row'
|
||||||
|
checked={row?.getIsSelected()}
|
||||||
|
onChange={row?.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('favorite', {
|
||||||
|
header: () => (
|
||||||
|
<FavoriteIconHeader
|
||||||
|
isActive={tableState.favoritesFirst}
|
||||||
|
onClick={() =>
|
||||||
|
setTableState({
|
||||||
|
favoritesFirst: !tableState.favoritesFirst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row: { original: feature } }) => (
|
||||||
|
<FavoriteIconCell
|
||||||
|
value={feature?.favorite}
|
||||||
|
onClick={() => onFavorite(feature)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
// hideInMenu: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('lastSeenAt', {
|
||||||
|
header: 'Last seen',
|
||||||
|
cell: ({ row: { original } }) => (
|
||||||
|
<MemoizedFeatureEnvironmentSeenCell
|
||||||
|
feature={original}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
size: 50,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('type', {
|
||||||
|
header: 'Type',
|
||||||
|
cell: FeatureTypeCell,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('name', {
|
||||||
|
header: 'Name',
|
||||||
|
cell: FeatureNameCell,
|
||||||
|
meta: {
|
||||||
|
width: '50%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('createdAt', {
|
||||||
|
header: 'Created',
|
||||||
|
cell: DateCell,
|
||||||
|
}),
|
||||||
|
...environments.map(
|
||||||
|
(projectEnvironment: ProjectEnvironmentType) => {
|
||||||
|
const name = projectEnvironment.environment;
|
||||||
|
const isChangeRequestEnabled =
|
||||||
|
isChangeRequestConfigured(name);
|
||||||
|
|
||||||
|
return columnHelper.accessor(
|
||||||
|
(row) => ({
|
||||||
|
featureId: row.name,
|
||||||
|
environment: row.environments?.find(
|
||||||
|
(featureEnvironment) =>
|
||||||
|
featureEnvironment.name === name,
|
||||||
|
),
|
||||||
|
someEnabledEnvironmentHasVariants:
|
||||||
|
row.environments?.some(
|
||||||
|
(featureEnvironment) =>
|
||||||
|
featureEnvironment.variantCount &&
|
||||||
|
featureEnvironment.variantCount > 0 &&
|
||||||
|
featureEnvironment.enabled,
|
||||||
|
) || false,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
id: `environment:${name}`,
|
||||||
|
header: loading ? '' : name,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const {
|
||||||
|
featureId,
|
||||||
|
environment,
|
||||||
|
someEnabledEnvironmentHasVariants,
|
||||||
|
} = getValue();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeatureToggleCell
|
||||||
|
value={environment?.enabled || false}
|
||||||
|
featureId={featureId}
|
||||||
|
someEnabledEnvironmentHasVariants={
|
||||||
|
someEnabledEnvironmentHasVariants
|
||||||
|
}
|
||||||
|
environment={environment}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentName={name}
|
||||||
|
isChangeRequestEnabled={
|
||||||
|
isChangeRequestEnabled
|
||||||
|
}
|
||||||
|
refetch={refetch}
|
||||||
|
onFeatureToggleSwitch={onFeatureToggle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[projectId, environments, loading, tableState.favoritesFirst, refetch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholderData = useMemo(
|
||||||
|
() =>
|
||||||
|
Array(tableState.limit)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, index) => ({
|
||||||
|
id: index,
|
||||||
|
type: '-',
|
||||||
|
name: `Feature name ${index}`,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
environments: [
|
||||||
|
{
|
||||||
|
name: 'production',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'production',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
[tableState.limit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
if (initialLoad || (loading && total)) {
|
||||||
|
return placeholderData;
|
||||||
|
}
|
||||||
|
return features;
|
||||||
|
}, [loading, features]);
|
||||||
|
|
||||||
|
const table = useReactTable(
|
||||||
|
withTableState(tableState, setTableState, {
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
enableRowSelection: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContent
|
||||||
|
disableLoading
|
||||||
|
disablePadding
|
||||||
|
header={
|
||||||
|
<ProjectFeatureTogglesHeader
|
||||||
|
isLoading={initialLoad}
|
||||||
|
totalItems={total}
|
||||||
|
searchQuery={tableState.query || ''}
|
||||||
|
onChangeSearchQuery={(query) => {
|
||||||
|
setTableState({ query });
|
||||||
|
}}
|
||||||
|
dataToExport={data}
|
||||||
|
environmentsToExport={environments.map(
|
||||||
|
({ environment }) => environment,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={bodyLoadingRef}
|
||||||
|
aria-busy={loading}
|
||||||
|
aria-live='polite'
|
||||||
|
>
|
||||||
|
<SearchHighlightProvider value={tableState.query || ''}>
|
||||||
|
<PaginatedTable
|
||||||
|
tableInstance={table}
|
||||||
|
totalItems={total}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
{/* <BatchSelectionActionsBar
|
||||||
|
count={Object.keys(selectedRowIds).length}
|
||||||
|
>
|
||||||
|
<ProjectFeaturesBatchActions
|
||||||
|
selectedIds={Object.keys(selectedRowIds)}
|
||||||
|
data={features}
|
||||||
|
projectId={projectId}
|
||||||
|
onResetSelection={() => toggleAllRowsSelected(false)}
|
||||||
|
/>
|
||||||
|
</BatchSelectionActionsBar> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
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 { type FeatureEnvironmentSchema } from 'openapi';
|
||||||
|
|
||||||
|
const StyledSwitchContainer = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'hasWarning',
|
||||||
|
})<{ hasWarning?: boolean }>(({ theme, hasWarning }) => ({
|
||||||
|
flexGrow: 0,
|
||||||
|
...flexRow,
|
||||||
|
justifyContent: 'center',
|
||||||
|
...(hasWarning && {
|
||||||
|
'::before': {
|
||||||
|
content: '""',
|
||||||
|
display: 'block',
|
||||||
|
width: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IFeatureToggleCellProps {
|
||||||
|
projectId: string;
|
||||||
|
environmentName: string;
|
||||||
|
isChangeRequestEnabled: boolean;
|
||||||
|
refetch: () => void;
|
||||||
|
onFeatureToggleSwitch: ReturnType<UseFeatureToggleSwitchType>['onToggle'];
|
||||||
|
value: boolean;
|
||||||
|
featureId: string;
|
||||||
|
environment?: FeatureEnvironmentSchema;
|
||||||
|
someEnabledEnvironmentHasVariants?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureToggleCellComponent = ({
|
||||||
|
value,
|
||||||
|
featureId,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
isChangeRequestEnabled,
|
||||||
|
someEnabledEnvironmentHasVariants,
|
||||||
|
refetch,
|
||||||
|
onFeatureToggleSwitch,
|
||||||
|
}: IFeatureToggleCellProps) => {
|
||||||
|
const hasWarning = useMemo(
|
||||||
|
() =>
|
||||||
|
someEnabledEnvironmentHasVariants &&
|
||||||
|
environment?.variantCount === 0 &&
|
||||||
|
environment?.enabled,
|
||||||
|
[someEnabledEnvironmentHasVariants, environment],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onToggle = (newState: boolean, onRollback: () => void) => {
|
||||||
|
onFeatureToggleSwitch(newState, {
|
||||||
|
projectId,
|
||||||
|
featureId,
|
||||||
|
environmentName: environment?.name || '',
|
||||||
|
environmentType: environment?.type,
|
||||||
|
hasStrategies: environment?.hasStrategies,
|
||||||
|
hasEnabledStrategies: environment?.hasEnabledStrategies,
|
||||||
|
isChangeRequestEnabled,
|
||||||
|
onRollback,
|
||||||
|
onSuccess: refetch,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledSwitchContainer hasWarning={hasWarning}>
|
||||||
|
<FeatureToggleSwitch
|
||||||
|
projectId={projectId}
|
||||||
|
value={value}
|
||||||
|
featureId={featureId}
|
||||||
|
environmentName={environment?.name || ''}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasWarning || false}
|
||||||
|
show={<VariantsWarningTooltip />}
|
||||||
|
/>
|
||||||
|
</StyledSwitchContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeatureToggleCell = React.memo(FeatureToggleCellComponent);
|
@ -0,0 +1,175 @@
|
|||||||
|
import { VFC, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import useLoading from 'hooks/useLoading';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { Add, FileDownload } from '@mui/icons-material';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { getCreateTogglePath } from 'utils/routePathHelpers';
|
||||||
|
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
|
import { FeatureSchema } from 'openapi';
|
||||||
|
|
||||||
|
interface IProjectFeatureTogglesHeaderProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
totalItems?: number;
|
||||||
|
searchQuery?: string;
|
||||||
|
onChangeSearchQuery?: (query: string) => void;
|
||||||
|
dataToExport?: Pick<FeatureSchema, 'name'>[];
|
||||||
|
environmentsToExport?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ProjectFeatureTogglesHeader: VFC<
|
||||||
|
IProjectFeatureTogglesHeaderProps
|
||||||
|
> = ({
|
||||||
|
isLoading,
|
||||||
|
totalItems,
|
||||||
|
searchQuery,
|
||||||
|
onChangeSearchQuery,
|
||||||
|
dataToExport,
|
||||||
|
environmentsToExport,
|
||||||
|
}) => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const headerLoadingRef = useLoading(isLoading || false);
|
||||||
|
const [showTitle, setShowTitle] = useState(true);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const featuresExportImportFlag = useUiFlag('featuresExportImport');
|
||||||
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handleSearch = (query: string) => {
|
||||||
|
onChangeSearchQuery?.(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={headerLoadingRef}
|
||||||
|
aria-busy={isLoading}
|
||||||
|
aria-live='polite'
|
||||||
|
sx={(theme) => ({
|
||||||
|
padding: `${theme.spacing(2.5)} ${theme.spacing(3.125)}`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<PageHeader
|
||||||
|
titleElement={
|
||||||
|
showTitle
|
||||||
|
? `Feature toggles ${
|
||||||
|
totalItems !== undefined ? `(${totalItems})` : ''
|
||||||
|
}`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isSmallScreen}
|
||||||
|
show={
|
||||||
|
<Search
|
||||||
|
data-loading
|
||||||
|
placeholder='Search and Filter'
|
||||||
|
expandable
|
||||||
|
initialValue={searchQuery || ''}
|
||||||
|
onChange={handleSearch}
|
||||||
|
onFocus={() => setShowTitle(false)}
|
||||||
|
onBlur={() => setShowTitle(true)}
|
||||||
|
hasFilters
|
||||||
|
id='projectFeatureToggles'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* FIXME: columns menu */}
|
||||||
|
{/* <ColumnsMenu
|
||||||
|
allColumns={allColumns}
|
||||||
|
staticColumns={staticColumns}
|
||||||
|
dividerAfter={['createdAt']}
|
||||||
|
dividerBefore={['Actions']}
|
||||||
|
isCustomized={isCustomColumns}
|
||||||
|
setHiddenColumns={setHiddenColumns}
|
||||||
|
onCustomize={() => setIsCustomColumns(true)}
|
||||||
|
/> */}
|
||||||
|
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={featuresExportImportFlag}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!isLoading}
|
||||||
|
show={
|
||||||
|
<ExportDialog
|
||||||
|
showExportDialog={
|
||||||
|
showExportDialog
|
||||||
|
}
|
||||||
|
data={dataToExport || []}
|
||||||
|
onClose={() =>
|
||||||
|
setShowExportDialog(false)
|
||||||
|
}
|
||||||
|
environments={
|
||||||
|
environmentsToExport || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<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={searchQuery || ''}
|
||||||
|
onChange={handleSearch}
|
||||||
|
hasFilters
|
||||||
|
id='projectFeatureToggles'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PageHeader>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -4,23 +4,27 @@ import { FC } from 'react';
|
|||||||
import { BATCH_SELECT } from 'utils/testIds';
|
import { BATCH_SELECT } from 'utils/testIds';
|
||||||
|
|
||||||
interface IRowSelectCellProps {
|
interface IRowSelectCellProps {
|
||||||
onChange: () => void;
|
onChange: (_?: unknown) => void;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
noPadding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledBoxCell = styled(Box)(({ theme }) => ({
|
const StyledBoxCell = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const RowSelectCell: FC<IRowSelectCellProps> = ({
|
export const RowSelectCell: FC<IRowSelectCellProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
checked,
|
checked,
|
||||||
title,
|
title,
|
||||||
|
noPadding,
|
||||||
}) => (
|
}) => (
|
||||||
<StyledBoxCell data-testid={BATCH_SELECT}>
|
<StyledBoxCell
|
||||||
|
data-testid={BATCH_SELECT}
|
||||||
|
sx={(theme) => ({ paddingLeft: noPadding ? 0 : theme.spacing(2) })}
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
title={title}
|
title={title}
|
||||||
|
3
frontend/src/types/react-table-v8.d.ts
vendored
3
frontend/src/types/react-table-v8.d.ts
vendored
@ -2,6 +2,7 @@ import '@tanstack/react-table';
|
|||||||
|
|
||||||
declare module '@tanstack/table-core' {
|
declare module '@tanstack/table-core' {
|
||||||
interface ColumnMeta<TData extends RowData, TValue> {
|
interface ColumnMeta<TData extends RowData, TValue> {
|
||||||
align: 'left' | 'center' | 'right';
|
align?: 'left' | 'center' | 'right';
|
||||||
|
width?: number | string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user