mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-23 13:46:45 +02:00
feat: simplified paginated overview
This commit is contained in:
parent
8a54644e4d
commit
bb016be577
@ -35,7 +35,11 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
|||||||
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
|
||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
||||||
import { IProject } from 'interfaces/project';
|
import { IProject } from 'interfaces/project';
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
import {
|
||||||
|
PaginatedTable,
|
||||||
|
TablePlaceholder,
|
||||||
|
VirtualizedTable,
|
||||||
|
} from 'component/common/Table';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
@ -76,6 +80,10 @@ import {
|
|||||||
ArrayParam,
|
ArrayParam,
|
||||||
withDefault,
|
withDefault,
|
||||||
} from 'use-query-params';
|
} from 'use-query-params';
|
||||||
|
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
||||||
|
import { withTableState } from 'utils/withTableState';
|
||||||
|
import { FeatureSchema } from 'openapi';
|
||||||
|
import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@ -89,6 +97,7 @@ interface IPaginatedProjectFeatureTogglesProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||||
|
const columnHelper = createColumnHelper<FeatureSchema>();
|
||||||
|
|
||||||
export const PaginatedProjectFeatureToggles = ({
|
export const PaginatedProjectFeatureToggles = ({
|
||||||
environments,
|
environments,
|
||||||
@ -118,529 +127,52 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
refreshInterval,
|
refreshInterval,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const onChange = refetch;
|
|
||||||
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
const bodyLoadingRef = useLoading(loading);
|
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 data = useMemo(() => features, [features]);
|
||||||
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(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
columnHelper.accessor('name', {
|
||||||
id: 'Select',
|
header: 'Name',
|
||||||
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
// cell: (cell) => <FeatureNameCell value={cell.row} />,
|
||||||
<Checkbox {...getToggleAllRowsSelectedProps()} />
|
cell: ({ row }) => (
|
||||||
),
|
|
||||||
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
|
<LinkCell
|
||||||
title={value}
|
title={row.original.name}
|
||||||
to={`/projects/${projectId}/features/${value}`}
|
subtitle={row.original.description || undefined}
|
||||||
/>
|
to={`/projects/${row.original.project}/features/${row.original.name}`}
|
||||||
</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 [
|
|
||||||
typeof env === 'string' ? env : env.environment,
|
|
||||||
{
|
|
||||||
name: env,
|
|
||||||
enabled: thisEnv?.enabled || false,
|
|
||||||
variantCount: thisEnv?.variantCount || 0,
|
|
||||||
lastSeenAt: thisEnv?.lastSeenAt,
|
|
||||||
type: thisEnv?.type,
|
|
||||||
hasStrategies: thisEnv?.hasStrategies,
|
|
||||||
hasEnabledStrategies:
|
|
||||||
thisEnv?.hasEnabledStrategies,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}),
|
}),
|
||||||
),
|
],
|
||||||
someEnabledEnvironmentHasVariants:
|
[tableState.favoritesFirst],
|
||||||
feature.environments?.some(
|
|
||||||
(featureEnvironment) =>
|
|
||||||
featureEnvironment.variantCount &&
|
|
||||||
featureEnvironment.variantCount > 0 &&
|
|
||||||
featureEnvironment.enabled,
|
|
||||||
) || false,
|
|
||||||
})),
|
|
||||||
[features, environments],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getSearchText, getSearchContext } = useSearch(
|
const table = useReactTable(
|
||||||
|
withTableState(tableState, setTableState, {
|
||||||
columns,
|
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,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageContent
|
<PageContent
|
||||||
disableLoading
|
disableLoading
|
||||||
disablePadding
|
disablePadding
|
||||||
className={styles.container}
|
|
||||||
style={{ ...paginatedStyles, ...style }}
|
|
||||||
header={
|
header={
|
||||||
<Box
|
<ProjectFeatureTogglesHeader
|
||||||
ref={headerLoadingRef}
|
totalItems={total}
|
||||||
aria-busy={initialLoad}
|
searchQuery={tableState.query || ''}
|
||||||
aria-live='polite'
|
onChangeSearchQuery={(query) =>
|
||||||
sx={(theme) => ({
|
setTableState({ query })
|
||||||
padding: `${theme.spacing(2.5)} ${theme.spacing(
|
|
||||||
3.125,
|
|
||||||
)}`,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PageHeader
|
|
||||||
titleElement={
|
|
||||||
showTitle
|
|
||||||
? `Feature toggles (${
|
|
||||||
total || rows.length
|
|
||||||
})`
|
|
||||||
: null
|
|
||||||
}
|
}
|
||||||
actions={
|
isLoading={initialLoad}
|
||||||
<>
|
dataToExport={features} // FIXME: selected columns?
|
||||||
<ConditionallyRender
|
environmentsToExport={environments.map(
|
||||||
condition={!isSmallScreen}
|
({ environment }) => environment, // FIXME: visible env columns?
|
||||||
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
|
<div
|
||||||
@ -648,104 +180,14 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
aria-busy={loading}
|
aria-busy={loading}
|
||||||
aria-live='polite'
|
aria-live='polite'
|
||||||
>
|
>
|
||||||
<SearchHighlightProvider
|
<SearchHighlightProvider value={tableState.query || ''}>
|
||||||
value={getSearchText(tableState.query || '')}
|
<PaginatedTable
|
||||||
>
|
tableInstance={table}
|
||||||
<VirtualizedTable
|
totalItems={total}
|
||||||
rows={rows}
|
|
||||||
headerGroups={headerGroups}
|
|
||||||
prepareRow={prepareRow}
|
|
||||||
/>
|
/>
|
||||||
</SearchHighlightProvider>
|
</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>
|
</div>
|
||||||
</PageContent>
|
</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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user