mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02:00
feat: Table with feature overview cell (#6713)
This commit is contained in:
parent
f89c2aa829
commit
6a0135a482
@ -28,8 +28,7 @@ const HeaderCell = <T extends object>(header: Header<T, unknown>) => {
|
|||||||
onClick={() => column.toggleSorting()}
|
onClick={() => column.toggleSorting()}
|
||||||
styles={{
|
styles={{
|
||||||
borderRadius: '0px',
|
borderRadius: '0px',
|
||||||
paddingTop: 0,
|
padding: 0,
|
||||||
paddingBottom: 0,
|
|
||||||
width,
|
width,
|
||||||
maxWidth: fixedWidth,
|
maxWidth: fixedWidth,
|
||||||
minWidth: fixedWidth,
|
minWidth: fixedWidth,
|
||||||
|
@ -5,12 +5,12 @@ import StarBorderIcon from '@mui/icons-material/StarBorder';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
const StyledCell = styled(Box)(({ theme }) => ({
|
const StyledCell = styled(Box)(({ theme }) => ({
|
||||||
paddingLeft: theme.spacing(1.25),
|
paddingRight: theme.spacing(0.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
padding: theme.spacing(1.25),
|
paddingRight: theme.spacing(0.5),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledIconButtonInactive = styled(StyledIconButton)({
|
const StyledIconButtonInactive = styled(StyledIconButton)({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import type { FeatureSearchResponseSchema } from '../../../../../openapi';
|
import type { FeatureSearchResponseSchema } from '../../../../../openapi';
|
||||||
import { Box, styled, Tooltip } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||||
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
||||||
import { useSearchHighlightContext } from '../../SearchHighlightContext/SearchHighlightContext';
|
import { useSearchHighlightContext } from '../../SearchHighlightContext/SearchHighlightContext';
|
||||||
@ -59,13 +59,13 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({
|
|||||||
placement='bottom-start'
|
placement='bottom-start'
|
||||||
arrow
|
arrow
|
||||||
>
|
>
|
||||||
<StyledDescription data-loading>
|
<StyledDescription>
|
||||||
<Highlighter search={searchQuery}>{text}</Highlighter>
|
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||||
</StyledDescription>
|
</StyledDescription>
|
||||||
</HtmlTooltip>
|
</HtmlTooltip>
|
||||||
}
|
}
|
||||||
elseShow={
|
elseShow={
|
||||||
<StyledDescription data-loading>
|
<StyledDescription>
|
||||||
<Highlighter search={searchQuery}>{text}</Highlighter>
|
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||||
</StyledDescription>
|
</StyledDescription>
|
||||||
}
|
}
|
||||||
@ -73,11 +73,26 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CappedTag: FC<{ tag: string }> = ({ tag }) => {
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={tag.length > 30}
|
||||||
|
show={
|
||||||
|
<HtmlTooltip title={tag}>
|
||||||
|
<Tag>{tag}</Tag>
|
||||||
|
</HtmlTooltip>
|
||||||
|
}
|
||||||
|
elseShow={<Tag>{tag}</Tag>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Container = styled(Box)(({ theme }) => ({
|
const Container = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: theme.spacing(0.5),
|
gap: theme.spacing(0.5),
|
||||||
margin: theme.spacing(1, 0, 1, 0),
|
margin: theme.spacing(1.25, 0, 1, 0),
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FeatureNameAndType = styled(Box)(({ theme }) => ({
|
const FeatureNameAndType = styled(Box)(({ theme }) => ({
|
||||||
@ -108,7 +123,6 @@ const FeatureName: FC<{
|
|||||||
<Box sx={(theme) => ({ fontWeight: theme.typography.fontWeightBold })}>
|
<Box sx={(theme) => ({ fontWeight: theme.typography.fontWeightBold })}>
|
||||||
<StyledFeatureLink to={`/projects/${project}/features/${feature}`}>
|
<StyledFeatureLink to={`/projects/${project}/features/${feature}`}>
|
||||||
<StyledTitle
|
<StyledTitle
|
||||||
data-loading
|
|
||||||
style={{
|
style={{
|
||||||
WebkitLineClamp: 1,
|
WebkitLineClamp: 1,
|
||||||
lineClamp: 1,
|
lineClamp: 1,
|
||||||
@ -136,9 +150,9 @@ const Tags: FC<{ tags: FeatureSearchResponseSchema['tags'] }> = ({ tags }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TagsContainer>
|
<TagsContainer>
|
||||||
{tag1 && <Tag>{tag1}</Tag>}
|
{tag1 && <CappedTag tag={tag1} />}
|
||||||
{tag2 && <Tag>{tag2}</Tag>}
|
{tag2 && <CappedTag tag={tag2} />}
|
||||||
{tag3 && <Tag>{tag3}</Tag>}
|
{tag3 && <CappedTag tag={tag3} />}
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={restTags.length > 0}
|
condition={restTags.length > 0}
|
||||||
show={<RestTags tags={restTags} />}
|
show={<RestTags tags={restTags} />}
|
||||||
@ -152,47 +166,32 @@ const PrimaryFeatureInfo: FC<{
|
|||||||
feature: string;
|
feature: string;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
type: string;
|
type: string;
|
||||||
}> = ({ project, feature, type, searchQuery }) => {
|
dependencyType: string;
|
||||||
|
}> = ({ project, feature, type, searchQuery, dependencyType }) => {
|
||||||
const { featureTypes } = useFeatureTypes();
|
const { featureTypes } = useFeatureTypes();
|
||||||
const IconComponent = getFeatureTypeIcons(type);
|
const IconComponent = getFeatureTypeIcons(type);
|
||||||
const typeName = featureTypes.find(
|
const typeName = featureTypes.find(
|
||||||
(featureType) => featureType.id === type,
|
(featureType) => featureType.id === type,
|
||||||
)?.name;
|
)?.name;
|
||||||
const title = `This is a "${typeName || type}" flag`;
|
const title = `${typeName || type} flag`;
|
||||||
|
|
||||||
const TypeIcon = () => (
|
const TypeIcon = () => (
|
||||||
<Tooltip arrow title={title} describeChild>
|
<HtmlTooltip arrow title={title} describeChild>
|
||||||
<IconComponent
|
<IconComponent sx={(theme) => ({ fontSize: theme.spacing(2) })} />
|
||||||
sx={(theme) => ({ fontSize: theme.spacing(2) })}
|
</HtmlTooltip>
|
||||||
data-loading
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeatureNameAndType>
|
<FeatureNameAndType data-loading>
|
||||||
<TypeIcon />
|
<TypeIcon />
|
||||||
<FeatureName
|
<FeatureName
|
||||||
project={project}
|
project={project}
|
||||||
feature={feature}
|
feature={feature}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
/>
|
/>
|
||||||
</FeatureNameAndType>
|
<ConditionallyRender
|
||||||
);
|
condition={Boolean(dependencyType)}
|
||||||
};
|
show={
|
||||||
|
|
||||||
const SecondaryFeatureInfo: FC<{
|
|
||||||
dependencyType: string;
|
|
||||||
description: string;
|
|
||||||
searchQuery: string;
|
|
||||||
}> = ({ dependencyType, description, searchQuery }) => {
|
|
||||||
return (
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(dependencyType) || Boolean(description)}
|
|
||||||
show={
|
|
||||||
<Box
|
|
||||||
sx={(theme) => ({ display: 'flex', gap: theme.spacing(1) })}
|
|
||||||
>
|
|
||||||
<DependencyBadge
|
<DependencyBadge
|
||||||
color={
|
color={
|
||||||
dependencyType === 'parent'
|
dependencyType === 'parent'
|
||||||
@ -202,6 +201,23 @@ const SecondaryFeatureInfo: FC<{
|
|||||||
>
|
>
|
||||||
{dependencyType}
|
{dependencyType}
|
||||||
</DependencyBadge>
|
</DependencyBadge>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FeatureNameAndType>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SecondaryFeatureInfo: FC<{
|
||||||
|
description: string;
|
||||||
|
searchQuery: string;
|
||||||
|
}> = ({ description, searchQuery }) => {
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(description)}
|
||||||
|
show={
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({ display: 'flex', gap: theme.spacing(1) })}
|
||||||
|
>
|
||||||
<CappedDescription
|
<CappedDescription
|
||||||
text={description}
|
text={description}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@ -222,10 +238,10 @@ export const FeatureOverviewCell: FC<IFeatureNameCellProps> = ({ row }) => {
|
|||||||
feature={row.original.name}
|
feature={row.original.name}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
type={row.original.type || ''}
|
type={row.original.type || ''}
|
||||||
|
dependencyType={row.original.dependencyType || ''}
|
||||||
/>
|
/>
|
||||||
<SecondaryFeatureInfo
|
<SecondaryFeatureInfo
|
||||||
description={row.original.description || ''}
|
description={row.original.description || ''}
|
||||||
dependencyType={row.original.dependencyType || ''}
|
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
/>
|
/>
|
||||||
<Tags tags={row.original.tags} />
|
<Tags tags={row.original.tags} />
|
||||||
|
@ -0,0 +1,495 @@
|
|||||||
|
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';
|
||||||
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
|
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||||
|
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 { 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 { 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 { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||||
|
import useLoading from 'hooks/useLoading';
|
||||||
|
import {
|
||||||
|
DEFAULT_PAGE_LIMIT,
|
||||||
|
useFeatureSearch,
|
||||||
|
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||||
|
import mapValues from 'lodash.mapvalues';
|
||||||
|
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
||||||
|
import {
|
||||||
|
BooleansStringParam,
|
||||||
|
FilterItemParam,
|
||||||
|
} from 'utils/serializeQueryParams';
|
||||||
|
import {
|
||||||
|
NumberParam,
|
||||||
|
StringParam,
|
||||||
|
ArrayParam,
|
||||||
|
withDefault,
|
||||||
|
encodeQueryParams,
|
||||||
|
} 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';
|
||||||
|
import { ProjectOverviewFilters } from './ProjectOverviewFilters';
|
||||||
|
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility';
|
||||||
|
import { TableEmptyState } from './TableEmptyState/TableEmptyState';
|
||||||
|
import { useRowActions } from './hooks/useRowActions';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||||
|
import { useSelectedData } from './hooks/useSelectedData';
|
||||||
|
|
||||||
|
interface IPaginatedProjectFeatureTogglesProps {
|
||||||
|
environments: string[];
|
||||||
|
refreshInterval?: number;
|
||||||
|
storageKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatEnvironmentColumnId = (environment: string) =>
|
||||||
|
`environment:${environment}`;
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
||||||
|
const getRowId = (row: { name: string }) => row.name;
|
||||||
|
|
||||||
|
export const OldProjectFeatureToggles = ({
|
||||||
|
environments,
|
||||||
|
refreshInterval = 15 * 1000,
|
||||||
|
storageKey = 'project-feature-toggles-v2',
|
||||||
|
}: IPaginatedProjectFeatureTogglesProps) => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
|
||||||
|
const featuresExportImport = useUiFlag('featuresExportImport');
|
||||||
|
|
||||||
|
const stateConfig = {
|
||||||
|
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,
|
||||||
|
tag: FilterItemParam,
|
||||||
|
createdAt: FilterItemParam,
|
||||||
|
};
|
||||||
|
const [tableState, setTableState] = usePersistentTableState(
|
||||||
|
`${storageKey}-${projectId}`,
|
||||||
|
stateConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterState = {
|
||||||
|
tag: tableState.tag,
|
||||||
|
createdAt: tableState.createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { features, total, refetch, loading, initialLoad } = useFeatureSearch(
|
||||||
|
mapValues(
|
||||||
|
{
|
||||||
|
...encodeQueryParams(stateConfig, tableState),
|
||||||
|
project: `IS:${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 {
|
||||||
|
rowActionsDialogs,
|
||||||
|
setFeatureArchiveState,
|
||||||
|
setFeatureStaleDialogState,
|
||||||
|
} = useRowActions(refetch, projectId);
|
||||||
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'select',
|
||||||
|
header: ({ table }) => (
|
||||||
|
<MemoizedRowSelectCell
|
||||||
|
title='Select all rows'
|
||||||
|
checked={table?.getIsAllRowsSelected()}
|
||||||
|
onChange={table?.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<MemoizedRowSelectCell
|
||||||
|
title='Select row'
|
||||||
|
checked={row?.getIsSelected()}
|
||||||
|
onChange={row?.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
width: '1%',
|
||||||
|
},
|
||||||
|
enableHiding: false,
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('favorite', {
|
||||||
|
id: 'favorite',
|
||||||
|
header: () => (
|
||||||
|
<FavoriteIconHeader
|
||||||
|
isActive={tableState.favoritesFirst}
|
||||||
|
onClick={() =>
|
||||||
|
setTableState({
|
||||||
|
favoritesFirst: !tableState.favoritesFirst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row: { original: feature } }) => (
|
||||||
|
<FavoriteIconCell
|
||||||
|
value={feature?.favorite}
|
||||||
|
onClick={() => onFavorite(feature)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
width: '1%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('lastSeenAt', {
|
||||||
|
id: 'lastSeenAt',
|
||||||
|
header: 'Last seen',
|
||||||
|
cell: ({ row: { original } }) => (
|
||||||
|
<MemoizedFeatureEnvironmentSeenCell
|
||||||
|
feature={original}
|
||||||
|
data-loading
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
size: 50,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
width: '1%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('type', {
|
||||||
|
id: 'type',
|
||||||
|
header: 'Type',
|
||||||
|
cell: FeatureTypeCell,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
width: '1%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('name', {
|
||||||
|
id: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
cell: FeatureNameCell,
|
||||||
|
enableHiding: false,
|
||||||
|
meta: {
|
||||||
|
width: '50%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('tags', {
|
||||||
|
id: 'tags',
|
||||||
|
header: 'Tags',
|
||||||
|
enableSorting: false,
|
||||||
|
cell: FeatureTagCell,
|
||||||
|
meta: {
|
||||||
|
width: '1%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('createdAt', {
|
||||||
|
id: 'createdAt',
|
||||||
|
header: 'Created',
|
||||||
|
cell: DateCell,
|
||||||
|
}),
|
||||||
|
...environments.map((name: string) => {
|
||||||
|
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: formatEnvironmentColumnId(name),
|
||||||
|
header: name,
|
||||||
|
meta: {
|
||||||
|
align: 'center',
|
||||||
|
width: 90,
|
||||||
|
},
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
header: '',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<ActionsCell
|
||||||
|
row={row}
|
||||||
|
projectId={projectId}
|
||||||
|
onOpenArchiveDialog={setFeatureArchiveState}
|
||||||
|
onOpenStaleDialog={setFeatureStaleDialogState}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
meta: {
|
||||||
|
align: 'right',
|
||||||
|
width: '1%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[projectId, environments, tableState.favoritesFirst, refetch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholderData = useMemo(
|
||||||
|
() =>
|
||||||
|
Array(tableState.limit)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, index) => ({
|
||||||
|
id: index,
|
||||||
|
type: '-',
|
||||||
|
name: `Feature name ${index}`,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
dependencyType: null,
|
||||||
|
favorite: false,
|
||||||
|
impressionData: false,
|
||||||
|
project: 'project',
|
||||||
|
segments: [],
|
||||||
|
stale: false,
|
||||||
|
environments: [
|
||||||
|
{
|
||||||
|
name: 'production',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'production',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
[tableState.limit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPlaceholder = Boolean(initialLoad || (loading && total));
|
||||||
|
const bodyLoadingRef = useLoading(isPlaceholder);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
if (isPlaceholder) {
|
||||||
|
return placeholderData;
|
||||||
|
}
|
||||||
|
return features;
|
||||||
|
}, [isPlaceholder, features]);
|
||||||
|
const allColumnIds = useMemo(
|
||||||
|
() => columns.map((column) => column.id).filter(Boolean) as string[],
|
||||||
|
[columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultColumnVisibility = useDefaultColumnVisibility(allColumnIds);
|
||||||
|
|
||||||
|
const table = useReactTable(
|
||||||
|
withTableState(tableState, setTableState, {
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
enableRowSelection: true,
|
||||||
|
state: {
|
||||||
|
columnVisibility: defaultColumnVisibility,
|
||||||
|
},
|
||||||
|
getRowId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { columnVisibility, rowSelection } = table.getState();
|
||||||
|
const onToggleColumnVisibility = useCallback(
|
||||||
|
(columnId) => {
|
||||||
|
const isVisible = columnVisibility[columnId];
|
||||||
|
const newColumnVisibility: Record<string, boolean> = {
|
||||||
|
...columnVisibility,
|
||||||
|
[columnId]: !isVisible,
|
||||||
|
};
|
||||||
|
setTableState({
|
||||||
|
columns: Object.keys(newColumnVisibility).filter(
|
||||||
|
(columnId) =>
|
||||||
|
newColumnVisibility[columnId] &&
|
||||||
|
!columnId.includes(','),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[columnVisibility, setTableState],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedData = useSelectedData(features, rowSelection);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageContent
|
||||||
|
disableLoading
|
||||||
|
disablePadding
|
||||||
|
header={
|
||||||
|
<ProjectFeatureTogglesHeader
|
||||||
|
isLoading={initialLoad}
|
||||||
|
totalItems={total}
|
||||||
|
searchQuery={tableState.query || ''}
|
||||||
|
onChangeSearchQuery={(query) => {
|
||||||
|
setTableState({ query });
|
||||||
|
}}
|
||||||
|
dataToExport={data}
|
||||||
|
environmentsToExport={environments}
|
||||||
|
actions={
|
||||||
|
<ColumnsMenu
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Last seen',
|
||||||
|
id: 'lastSeenAt',
|
||||||
|
isVisible: columnVisibility.lastSeenAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Type',
|
||||||
|
id: 'type',
|
||||||
|
isVisible: columnVisibility.type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Name',
|
||||||
|
id: 'name',
|
||||||
|
isVisible: columnVisibility.name,
|
||||||
|
isStatic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Tags',
|
||||||
|
id: 'tags',
|
||||||
|
isVisible: columnVisibility.tags,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Created',
|
||||||
|
id: 'createdAt',
|
||||||
|
isVisible: columnVisibility.createdAt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'divider',
|
||||||
|
},
|
||||||
|
...environments.map((environment) => ({
|
||||||
|
header: environment,
|
||||||
|
id: formatEnvironmentColumnId(
|
||||||
|
environment,
|
||||||
|
),
|
||||||
|
isVisible:
|
||||||
|
columnVisibility[
|
||||||
|
formatEnvironmentColumnId(
|
||||||
|
environment,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
onToggle={onToggleColumnVisibility}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
bodyClass='noop'
|
||||||
|
style={{ cursor: 'inherit' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={bodyLoadingRef}
|
||||||
|
aria-busy={isPlaceholder}
|
||||||
|
aria-live='polite'
|
||||||
|
>
|
||||||
|
<ProjectOverviewFilters
|
||||||
|
onChange={setTableState}
|
||||||
|
state={filterState}
|
||||||
|
/>
|
||||||
|
<SearchHighlightProvider value={tableState.query || ''}>
|
||||||
|
<PaginatedTable
|
||||||
|
tableInstance={table}
|
||||||
|
totalItems={total}
|
||||||
|
/>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={!data.length && !isPlaceholder}
|
||||||
|
show={
|
||||||
|
<TableEmptyState query={tableState.query || ''} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{rowActionsDialogs}
|
||||||
|
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={featuresExportImport && !loading}
|
||||||
|
show={
|
||||||
|
// TODO: `export all` backend
|
||||||
|
<ExportDialog
|
||||||
|
showExportDialog={showExportDialog}
|
||||||
|
data={data}
|
||||||
|
onClose={() => setShowExportDialog(false)}
|
||||||
|
environments={environments}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{featureToggleModals}
|
||||||
|
</div>
|
||||||
|
</PageContent>
|
||||||
|
<BatchSelectionActionsBar count={selectedData.length}>
|
||||||
|
<ProjectFeaturesBatchActions
|
||||||
|
selectedIds={Object.keys(rowSelection)}
|
||||||
|
data={selectedData}
|
||||||
|
projectId={projectId}
|
||||||
|
onResetSelection={table.resetRowSelection}
|
||||||
|
onChange={refetch}
|
||||||
|
/>
|
||||||
|
</BatchSelectionActionsBar>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -3,7 +3,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
|
||||||
import { PaginatedTable } from 'component/common/Table';
|
import { PaginatedTable } from 'component/common/Table';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||||
@ -30,25 +29,24 @@ import {
|
|||||||
FilterItemParam,
|
FilterItemParam,
|
||||||
} from 'utils/serializeQueryParams';
|
} from 'utils/serializeQueryParams';
|
||||||
import {
|
import {
|
||||||
|
ArrayParam,
|
||||||
|
encodeQueryParams,
|
||||||
NumberParam,
|
NumberParam,
|
||||||
StringParam,
|
StringParam,
|
||||||
ArrayParam,
|
|
||||||
withDefault,
|
withDefault,
|
||||||
encodeQueryParams,
|
|
||||||
} from 'use-query-params';
|
} from 'use-query-params';
|
||||||
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
|
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
|
||||||
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
||||||
import { withTableState } from 'utils/withTableState';
|
import { withTableState } from 'utils/withTableState';
|
||||||
import type { FeatureSearchResponseSchema } from 'openapi';
|
import type { FeatureSearchResponseSchema } from 'openapi';
|
||||||
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
|
||||||
import { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell';
|
import { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell';
|
||||||
import { ProjectOverviewFilters } from './ProjectOverviewFilters';
|
import { ProjectOverviewFilters } from './ProjectOverviewFilters';
|
||||||
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility';
|
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility';
|
||||||
import { TableEmptyState } from './TableEmptyState/TableEmptyState';
|
import { TableEmptyState } from './TableEmptyState/TableEmptyState';
|
||||||
import { useRowActions } from './hooks/useRowActions';
|
import { useRowActions } from './hooks/useRowActions';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
|
||||||
import { useSelectedData } from './hooks/useSelectedData';
|
import { useSelectedData } from './hooks/useSelectedData';
|
||||||
|
import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell';
|
||||||
|
|
||||||
interface IPaginatedProjectFeatureTogglesProps {
|
interface IPaginatedProjectFeatureTogglesProps {
|
||||||
environments: string[];
|
environments: string[];
|
||||||
@ -133,7 +131,6 @@ export const ProjectFeatureToggles = ({
|
|||||||
id: 'select',
|
id: 'select',
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
<MemoizedRowSelectCell
|
<MemoizedRowSelectCell
|
||||||
noPadding
|
|
||||||
title='Select all rows'
|
title='Select all rows'
|
||||||
checked={table?.getIsAllRowsSelected()}
|
checked={table?.getIsAllRowsSelected()}
|
||||||
onChange={table?.getToggleAllRowsSelectedHandler()}
|
onChange={table?.getToggleAllRowsSelectedHandler()}
|
||||||
@ -141,7 +138,6 @@ export const ProjectFeatureToggles = ({
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<MemoizedRowSelectCell
|
<MemoizedRowSelectCell
|
||||||
noPadding
|
|
||||||
title='Select row'
|
title='Select row'
|
||||||
checked={row?.getIsSelected()}
|
checked={row?.getIsSelected()}
|
||||||
onChange={row?.getToggleSelectedHandler()}
|
onChange={row?.getToggleSelectedHandler()}
|
||||||
@ -177,6 +173,23 @@ export const ProjectFeatureToggles = ({
|
|||||||
width: '1%',
|
width: '1%',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
columnHelper.accessor('name', {
|
||||||
|
id: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
cell: FeatureOverviewCell,
|
||||||
|
enableHiding: false,
|
||||||
|
meta: {
|
||||||
|
width: '50%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('createdAt', {
|
||||||
|
id: 'createdAt',
|
||||||
|
header: 'Created',
|
||||||
|
cell: DateCell,
|
||||||
|
meta: {
|
||||||
|
width: '1%',
|
||||||
|
},
|
||||||
|
}),
|
||||||
columnHelper.accessor('lastSeenAt', {
|
columnHelper.accessor('lastSeenAt', {
|
||||||
id: 'lastSeenAt',
|
id: 'lastSeenAt',
|
||||||
header: 'Last seen',
|
header: 'Last seen',
|
||||||
@ -192,38 +205,6 @@ export const ProjectFeatureToggles = ({
|
|||||||
width: '1%',
|
width: '1%',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('type', {
|
|
||||||
id: 'type',
|
|
||||||
header: 'Type',
|
|
||||||
cell: FeatureTypeCell,
|
|
||||||
meta: {
|
|
||||||
align: 'center',
|
|
||||||
width: '1%',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('name', {
|
|
||||||
id: 'name',
|
|
||||||
header: 'Name',
|
|
||||||
cell: FeatureNameCell,
|
|
||||||
enableHiding: false,
|
|
||||||
meta: {
|
|
||||||
width: '50%',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('tags', {
|
|
||||||
id: 'tags',
|
|
||||||
header: 'Tags',
|
|
||||||
enableSorting: false,
|
|
||||||
cell: FeatureTagCell,
|
|
||||||
meta: {
|
|
||||||
width: '1%',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('createdAt', {
|
|
||||||
id: 'createdAt',
|
|
||||||
header: 'Created',
|
|
||||||
cell: DateCell,
|
|
||||||
}),
|
|
||||||
...environments.map((name: string) => {
|
...environments.map((name: string) => {
|
||||||
const isChangeRequestEnabled = isChangeRequestConfigured(name);
|
const isChangeRequestEnabled = isChangeRequestConfigured(name);
|
||||||
|
|
||||||
@ -395,32 +376,22 @@ export const ProjectFeatureToggles = ({
|
|||||||
actions={
|
actions={
|
||||||
<ColumnsMenu
|
<ColumnsMenu
|
||||||
columns={[
|
columns={[
|
||||||
{
|
|
||||||
header: 'Last seen',
|
|
||||||
id: 'lastSeenAt',
|
|
||||||
isVisible: columnVisibility.lastSeenAt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Type',
|
|
||||||
id: 'type',
|
|
||||||
isVisible: columnVisibility.type,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
id: 'name',
|
id: 'name',
|
||||||
isVisible: columnVisibility.name,
|
isVisible: columnVisibility.name,
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: 'Tags',
|
|
||||||
id: 'tags',
|
|
||||||
isVisible: columnVisibility.tags,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'Created',
|
header: 'Created',
|
||||||
id: 'createdAt',
|
id: 'createdAt',
|
||||||
isVisible: columnVisibility.createdAt,
|
isVisible: columnVisibility.createdAt,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Last seen',
|
||||||
|
id: 'lastSeenAt',
|
||||||
|
isVisible: columnVisibility.lastSeenAt,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'divider',
|
id: 'divider',
|
||||||
},
|
},
|
||||||
|
@ -7,24 +7,21 @@ interface IRowSelectCellProps {
|
|||||||
onChange: (_?: unknown) => 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),
|
||||||
|
paddingRight: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const RowSelectCell: FC<IRowSelectCellProps> = ({
|
export const RowSelectCell: FC<IRowSelectCellProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
checked,
|
checked,
|
||||||
title,
|
title,
|
||||||
noPadding,
|
|
||||||
}) => (
|
}) => (
|
||||||
<StyledBoxCell
|
<StyledBoxCell data-testid={BATCH_SELECT}>
|
||||||
data-testid={BATCH_SELECT}
|
|
||||||
sx={(theme) => ({ paddingLeft: noPadding ? 0 : theme.spacing(2) })}
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
title={title}
|
title={title}
|
||||||
|
@ -11,6 +11,7 @@ import { usePageTitle } from 'hooks/usePageTitle';
|
|||||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { ProjectOverviewChangeRequests } from './ProjectOverviewChangeRequests';
|
import { ProjectOverviewChangeRequests } from './ProjectOverviewChangeRequests';
|
||||||
|
import { OldProjectFeatureToggles } from './PaginatedProjectFeatureToggles/OldProjectFeatureToggles';
|
||||||
|
|
||||||
const refreshInterval = 15 * 1000;
|
const refreshInterval = 15 * 1000;
|
||||||
|
|
||||||
@ -85,8 +86,8 @@ const OldProjectOverview: FC<{
|
|||||||
<ProjectStats stats={project.stats} />
|
<ProjectStats stats={project.stats} />
|
||||||
|
|
||||||
<StyledProjectToggles>
|
<StyledProjectToggles>
|
||||||
<ProjectFeatureToggles
|
<OldProjectFeatureToggles
|
||||||
environments={environments.map(
|
environments={project.environments.map(
|
||||||
(environment) => environment.environment,
|
(environment) => environment.environment,
|
||||||
)}
|
)}
|
||||||
refreshInterval={refreshInterval}
|
refreshInterval={refreshInterval}
|
||||||
|
Loading…
Reference in New Issue
Block a user