mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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()} | ||||
|             styles={{ | ||||
|                 borderRadius: '0px', | ||||
|                 paddingTop: 0, | ||||
|                 paddingBottom: 0, | ||||
|                 padding: 0, | ||||
|                 width, | ||||
|                 maxWidth: fixedWidth, | ||||
|                 minWidth: fixedWidth, | ||||
|  | ||||
| @ -5,12 +5,12 @@ import StarBorderIcon from '@mui/icons-material/StarBorder'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| const StyledCell = styled(Box)(({ theme }) => ({ | ||||
|     paddingLeft: theme.spacing(1.25), | ||||
|     paddingRight: theme.spacing(0.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledIconButton = styled(IconButton)(({ theme }) => ({ | ||||
|     color: theme.palette.primary.main, | ||||
|     padding: theme.spacing(1.25), | ||||
|     paddingRight: theme.spacing(0.5), | ||||
| })); | ||||
| 
 | ||||
| const StyledIconButtonInactive = styled(StyledIconButton)({ | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import type { FC } from 'react'; | ||||
| 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 { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; | ||||
| import { useSearchHighlightContext } from '../../SearchHighlightContext/SearchHighlightContext'; | ||||
| @ -59,13 +59,13 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ | ||||
|                     placement='bottom-start' | ||||
|                     arrow | ||||
|                 > | ||||
|                     <StyledDescription data-loading> | ||||
|                     <StyledDescription> | ||||
|                         <Highlighter search={searchQuery}>{text}</Highlighter> | ||||
|                     </StyledDescription> | ||||
|                 </HtmlTooltip> | ||||
|             } | ||||
|             elseShow={ | ||||
|                 <StyledDescription data-loading> | ||||
|                 <StyledDescription> | ||||
|                     <Highlighter search={searchQuery}>{text}</Highlighter> | ||||
|                 </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 }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     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 }) => ({ | ||||
| @ -108,7 +123,6 @@ const FeatureName: FC<{ | ||||
|         <Box sx={(theme) => ({ fontWeight: theme.typography.fontWeightBold })}> | ||||
|             <StyledFeatureLink to={`/projects/${project}/features/${feature}`}> | ||||
|                 <StyledTitle | ||||
|                     data-loading | ||||
|                     style={{ | ||||
|                         WebkitLineClamp: 1, | ||||
|                         lineClamp: 1, | ||||
| @ -136,9 +150,9 @@ const Tags: FC<{ tags: FeatureSearchResponseSchema['tags'] }> = ({ tags }) => { | ||||
| 
 | ||||
|     return ( | ||||
|         <TagsContainer> | ||||
|             {tag1 && <Tag>{tag1}</Tag>} | ||||
|             {tag2 && <Tag>{tag2}</Tag>} | ||||
|             {tag3 && <Tag>{tag3}</Tag>} | ||||
|             {tag1 && <CappedTag tag={tag1} />} | ||||
|             {tag2 && <CappedTag tag={tag2} />} | ||||
|             {tag3 && <CappedTag tag={tag3} />} | ||||
|             <ConditionallyRender | ||||
|                 condition={restTags.length > 0} | ||||
|                 show={<RestTags tags={restTags} />} | ||||
| @ -152,47 +166,32 @@ const PrimaryFeatureInfo: FC<{ | ||||
|     feature: string; | ||||
|     searchQuery: string; | ||||
|     type: string; | ||||
| }> = ({ project, feature, type, searchQuery }) => { | ||||
|     dependencyType: string; | ||||
| }> = ({ project, feature, type, searchQuery, dependencyType }) => { | ||||
|     const { featureTypes } = useFeatureTypes(); | ||||
|     const IconComponent = getFeatureTypeIcons(type); | ||||
|     const typeName = featureTypes.find( | ||||
|         (featureType) => featureType.id === type, | ||||
|     )?.name; | ||||
|     const title = `This is a "${typeName || type}" flag`; | ||||
|     const title = `${typeName || type} flag`; | ||||
| 
 | ||||
|     const TypeIcon = () => ( | ||||
|         <Tooltip arrow title={title} describeChild> | ||||
|             <IconComponent | ||||
|                 sx={(theme) => ({ fontSize: theme.spacing(2) })} | ||||
|                 data-loading | ||||
|             /> | ||||
|         </Tooltip> | ||||
|         <HtmlTooltip arrow title={title} describeChild> | ||||
|             <IconComponent sx={(theme) => ({ fontSize: theme.spacing(2) })} /> | ||||
|         </HtmlTooltip> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|         <FeatureNameAndType> | ||||
|         <FeatureNameAndType data-loading> | ||||
|             <TypeIcon /> | ||||
|             <FeatureName | ||||
|                 project={project} | ||||
|                 feature={feature} | ||||
|                 searchQuery={searchQuery} | ||||
|             /> | ||||
|         </FeatureNameAndType> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| 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) })} | ||||
|                 > | ||||
|             <ConditionallyRender | ||||
|                 condition={Boolean(dependencyType)} | ||||
|                 show={ | ||||
|                     <DependencyBadge | ||||
|                         color={ | ||||
|                             dependencyType === 'parent' | ||||
| @ -202,6 +201,23 @@ const SecondaryFeatureInfo: FC<{ | ||||
|                     > | ||||
|                         {dependencyType} | ||||
|                     </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 | ||||
|                         text={description} | ||||
|                         searchQuery={searchQuery} | ||||
| @ -222,10 +238,10 @@ export const FeatureOverviewCell: FC<IFeatureNameCellProps> = ({ row }) => { | ||||
|                 feature={row.original.name} | ||||
|                 searchQuery={searchQuery} | ||||
|                 type={row.original.type || ''} | ||||
|                 dependencyType={row.original.dependencyType || ''} | ||||
|             /> | ||||
|             <SecondaryFeatureInfo | ||||
|                 description={row.original.description || ''} | ||||
|                 dependencyType={row.original.dependencyType || ''} | ||||
|                 searchQuery={searchQuery} | ||||
|             /> | ||||
|             <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 { 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'; | ||||
| @ -30,25 +29,24 @@ import { | ||||
|     FilterItemParam, | ||||
| } from 'utils/serializeQueryParams'; | ||||
| import { | ||||
|     ArrayParam, | ||||
|     encodeQueryParams, | ||||
|     NumberParam, | ||||
|     StringParam, | ||||
|     ArrayParam, | ||||
|     withDefault, | ||||
|     encodeQueryParams, | ||||
| } from 'use-query-params'; | ||||
| import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; | ||||
| import { createColumnHelper, useReactTable } from '@tanstack/react-table'; | ||||
| 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'; | ||||
| import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell'; | ||||
| 
 | ||||
| interface IPaginatedProjectFeatureTogglesProps { | ||||
|     environments: string[]; | ||||
| @ -133,7 +131,6 @@ export const ProjectFeatureToggles = ({ | ||||
|                 id: 'select', | ||||
|                 header: ({ table }) => ( | ||||
|                     <MemoizedRowSelectCell | ||||
|                         noPadding | ||||
|                         title='Select all rows' | ||||
|                         checked={table?.getIsAllRowsSelected()} | ||||
|                         onChange={table?.getToggleAllRowsSelectedHandler()} | ||||
| @ -141,7 +138,6 @@ export const ProjectFeatureToggles = ({ | ||||
|                 ), | ||||
|                 cell: ({ row }) => ( | ||||
|                     <MemoizedRowSelectCell | ||||
|                         noPadding | ||||
|                         title='Select row' | ||||
|                         checked={row?.getIsSelected()} | ||||
|                         onChange={row?.getToggleSelectedHandler()} | ||||
| @ -177,6 +173,23 @@ export const ProjectFeatureToggles = ({ | ||||
|                     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', { | ||||
|                 id: 'lastSeenAt', | ||||
|                 header: 'Last seen', | ||||
| @ -192,38 +205,6 @@ export const ProjectFeatureToggles = ({ | ||||
|                     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); | ||||
| 
 | ||||
| @ -395,32 +376,22 @@ export const ProjectFeatureToggles = ({ | ||||
|                         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, | ||||
|                                     }, | ||||
|                                     { | ||||
|                                         header: 'Last seen', | ||||
|                                         id: 'lastSeenAt', | ||||
|                                         isVisible: columnVisibility.lastSeenAt, | ||||
|                                     }, | ||||
|                                     { | ||||
|                                         id: 'divider', | ||||
|                                     }, | ||||
|  | ||||
| @ -7,24 +7,21 @@ interface IRowSelectCellProps { | ||||
|     onChange: (_?: unknown) => void; | ||||
|     checked: boolean; | ||||
|     title: string; | ||||
|     noPadding?: boolean; | ||||
| } | ||||
| 
 | ||||
| const StyledBoxCell = styled(Box)(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     justifyContent: 'center', | ||||
|     paddingLeft: theme.spacing(2), | ||||
|     paddingRight: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| export const RowSelectCell: FC<IRowSelectCellProps> = ({ | ||||
|     onChange, | ||||
|     checked, | ||||
|     title, | ||||
|     noPadding, | ||||
| }) => ( | ||||
|     <StyledBoxCell | ||||
|         data-testid={BATCH_SELECT} | ||||
|         sx={(theme) => ({ paddingLeft: noPadding ? 0 : theme.spacing(2) })} | ||||
|     > | ||||
|     <StyledBoxCell data-testid={BATCH_SELECT}> | ||||
|         <Checkbox | ||||
|             onChange={onChange} | ||||
|             title={title} | ||||
|  | ||||
| @ -11,6 +11,7 @@ import { usePageTitle } from 'hooks/usePageTitle'; | ||||
| import { useLastViewedProject } from 'hooks/useLastViewedProject'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import { ProjectOverviewChangeRequests } from './ProjectOverviewChangeRequests'; | ||||
| import { OldProjectFeatureToggles } from './PaginatedProjectFeatureToggles/OldProjectFeatureToggles'; | ||||
| 
 | ||||
| const refreshInterval = 15 * 1000; | ||||
| 
 | ||||
| @ -85,8 +86,8 @@ const OldProjectOverview: FC<{ | ||||
|                 <ProjectStats stats={project.stats} /> | ||||
| 
 | ||||
|                 <StyledProjectToggles> | ||||
|                     <ProjectFeatureToggles | ||||
|                         environments={environments.map( | ||||
|                     <OldProjectFeatureToggles | ||||
|                         environments={project.environments.map( | ||||
|                             (environment) => environment.environment, | ||||
|                         )} | ||||
|                         refreshInterval={refreshInterval} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user