1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Project Overview with react-table v8 (#5571)

This commit is contained in:
Tymoteusz Czech 2023-12-11 13:33:11 +01:00 committed by GitHub
parent ba50d1ef69
commit d11aedc12f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 739 additions and 23 deletions

View File

@ -14,6 +14,7 @@ const HeaderCell = <T extends object>(header: Header<T, unknown>) => {
const column = header.column;
const isDesc = column.getIsSorted() === 'desc';
const align = column.columnDef.meta?.align || undefined;
const width = column.columnDef.meta?.width || undefined;
return (
<CellSortable
@ -22,7 +23,12 @@ const HeaderCell = <T extends object>(header: Header<T, unknown>) => {
isDescending={isDesc}
align={align}
onClick={() => column.toggleSorting()}
styles={{ borderRadius: '0px' }}
styles={{
borderRadius: '0px',
paddingTop: 0,
paddingBottom: 0,
width,
}}
>
{header.isPlaceholder
? null

View File

@ -5,11 +5,14 @@ import { getLocalizedDateString } from '../../../util';
interface IDateCellProps {
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 date = getLocalizedDateString(value, locationSettings.locale);
const date = getLocalizedDateString(input, locationSettings.locale);
return <TextCell lineClamp={1}>{date}</TextCell>;
};

View File

@ -4,17 +4,21 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
interface IFeatureNameCellProps {
row: {
original: {
name: string;
description: string;
project: string;
name?: string | null;
description?: string | null;
project?: string | null;
};
};
}
export const FeatureNameCell: VFC<IFeatureNameCellProps> = ({ row }) => (
<LinkCell
title={row.original.name}
subtitle={row.original.description}
to={`/projects/${row.original.project}/features/${row.original.name}`}
title={row.original.name || ''}
subtitle={row.original.description || ''}
to={
row.original.project && row.original.name
? `/projects/${row.original.project}/features/${row.original.name}`
: undefined
}
/>
);

View File

@ -1,9 +1,12 @@
import React, { VFC } from 'react';
import { FeatureEnvironmentSeen } from 'component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen';
import { FeatureSchema } from 'openapi';
import { FeatureEnvironmentSchema } from 'openapi';
interface IFeatureSeenCellProps {
feature: FeatureSchema;
feature: {
environments?: FeatureEnvironmentSchema[];
lastSeenAt?: string | null;
};
}
export const FeatureEnvironmentSeenCell: VFC<IFeatureSeenCellProps> = ({

View File

@ -5,6 +5,7 @@ import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
interface IFeatureTypeProps {
value?: string;
getValue?: () => string | undefined | null;
}
const StyledContainer = styled('div')(({ theme }) => ({
@ -15,15 +16,20 @@ const StyledContainer = styled('div')(({ theme }) => ({
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 IconComponent = getFeatureTypeIcons(value);
const IconComponent = getFeatureTypeIcons(type);
const typeName = featureTypes
.filter((type) => type.id === value)
.map((type) => type.name);
const typeName = featureTypes.find(
(featureType) => featureType.id === type,
)?.name;
const title = `This is a "${typeName || value}" toggle`;
const title = `This is a "${typeName || type}" toggle`;
return (
<StyledContainer>

View File

@ -9,7 +9,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useLastViewedProject } from 'hooks/useLastViewedProject';
import { useUiFlag } from 'hooks/useUiFlag';
import { PaginatedProjectFeatureToggles } from '../ProjectFeatureToggles/PaginatedProjectFeatureToggles';
import { ExperimentalProjectFeatureToggles } from './ExperimentalProjectTable/ExperimentalProjectTable';
const refreshInterval = 15 * 1000;
@ -44,7 +44,7 @@ const PaginatedProjectOverview = () => {
<StyledContainer>
<StyledContentContainer>
<StyledProjectToggles>
<PaginatedProjectFeatureToggles
<ExperimentalProjectFeatureToggles
style={{ width: '100%', margin: 0 }}
environments={environments}
storageKey='project-features'

View File

@ -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
&ldquo;
{tableState.query}
&rdquo;
</TablePlaceholder>
</Box>
}
elseShow={
<Box sx={{ padding: theme.spacing(3) }}>
<TablePlaceholder>
No feature toggles available. Get
started by adding a new feature
toggle.
</TablePlaceholder>
</Box>
}
/>
}
/>
<FeatureStaleDialog
isStale={featureStaleDialogState.stale === true}
isOpen={Boolean(featureStaleDialogState.featureId)}
onClose={() => {
setFeatureStaleDialogState({});
onChange();
}}
featureId={featureStaleDialogState.featureId || ''}
projectId={projectId}
/>
<FeatureArchiveDialog
isOpen={Boolean(featureArchiveState)}
onConfirm={onChange}
onClose={() => {
setFeatureArchiveState(undefined);
}}
featureIds={[featureArchiveState || '']}
projectId={projectId}
/>
<ConditionallyRender
condition={
Boolean(uiConfig?.flags?.featuresExportImport) &&
!loading
}
show={
<ExportDialog
showExportDialog={showExportDialog}
data={data}
onClose={() => setShowExportDialog(false)}
environments={environments.map(
({ environment }) => environment,
)}
/>
}
/>
{featureToggleModals} */}
</div>
</PageContent>
{/* <BatchSelectionActionsBar
count={Object.keys(selectedRowIds).length}
>
<ProjectFeaturesBatchActions
selectedIds={Object.keys(selectedRowIds)}
data={features}
projectId={projectId}
onResetSelection={() => toggleAllRowsSelected(false)}
/>
</BatchSelectionActionsBar> */}
</>
);
};

View File

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

View File

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

View File

@ -4,23 +4,27 @@ import { FC } from 'react';
import { BATCH_SELECT } from 'utils/testIds';
interface IRowSelectCellProps {
onChange: () => void;
onChange: (_?: unknown) => void;
checked: boolean;
title: string;
noPadding?: boolean;
}
const StyledBoxCell = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
paddingLeft: theme.spacing(2),
}));
export const RowSelectCell: FC<IRowSelectCellProps> = ({
onChange,
checked,
title,
noPadding,
}) => (
<StyledBoxCell data-testid={BATCH_SELECT}>
<StyledBoxCell
data-testid={BATCH_SELECT}
sx={(theme) => ({ paddingLeft: noPadding ? 0 : theme.spacing(2) })}
>
<Checkbox
onChange={onChange}
title={title}

View File

@ -2,6 +2,7 @@ import '@tanstack/react-table';
declare module '@tanstack/table-core' {
interface ColumnMeta<TData extends RowData, TValue> {
align: 'left' | 'center' | 'right';
align?: 'left' | 'center' | 'right';
width?: number | string;
}
}