mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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 { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; | ||||
| 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 { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog'; | ||||
| import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; | ||||
| @ -76,6 +80,10 @@ import { | ||||
|     ArrayParam, | ||||
|     withDefault, | ||||
| } 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)(() => ({ | ||||
|     whiteSpace: 'nowrap', | ||||
| @ -89,6 +97,7 @@ interface IPaginatedProjectFeatureTogglesProps { | ||||
| } | ||||
| 
 | ||||
| const staticColumns = ['Select', 'Actions', 'name', 'favorite']; | ||||
| const columnHelper = createColumnHelper<FeatureSchema>(); | ||||
| 
 | ||||
| export const PaginatedProjectFeatureToggles = ({ | ||||
|     environments, | ||||
| @ -118,529 +127,52 @@ export const PaginatedProjectFeatureToggles = ({ | ||||
|             refreshInterval, | ||||
|         }, | ||||
|     ); | ||||
|     const onChange = refetch; | ||||
| 
 | ||||
|     const { classes: styles } = useStyles(); | ||||
|     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 { 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 data = useMemo(() => features, [features]); | ||||
| 
 | ||||
|     const columns = useMemo( | ||||
|         () => [ | ||||
|             { | ||||
|                 id: 'Select', | ||||
|                 Header: ({ getToggleAllRowsSelectedProps }: any) => ( | ||||
|                     <Checkbox {...getToggleAllRowsSelectedProps()} /> | ||||
|                 ), | ||||
|                 Cell: ({ row }: any) => ( | ||||
|                     <MemoizedRowSelectCell | ||||
|                         {...row?.getToggleRowSelectedProps?.()} | ||||
|             columnHelper.accessor('name', { | ||||
|                 header: 'Name', | ||||
|                 // cell: (cell) => <FeatureNameCell value={cell.row} />,
 | ||||
|                 cell: ({ row }) => ( | ||||
|                     <LinkCell | ||||
|                         title={row.original.name} | ||||
|                         subtitle={row.original.description || undefined} | ||||
|                         to={`/projects/${row.original.project}/features/${row.original.name}`} | ||||
|                     /> | ||||
|                 ), | ||||
|                 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 | ||||
|                                 title={value} | ||||
|                                 to={`/projects/${projectId}/features/${value}`} | ||||
|                             /> | ||||
|                         </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], | ||||
|         [tableState.favoritesFirst], | ||||
|     ); | ||||
| 
 | ||||
|     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: | ||||
|                     feature.environments?.some( | ||||
|                         (featureEnvironment) => | ||||
|                             featureEnvironment.variantCount && | ||||
|                             featureEnvironment.variantCount > 0 && | ||||
|                             featureEnvironment.enabled, | ||||
|                     ) || false, | ||||
|             })), | ||||
|         [features, environments], | ||||
|     ); | ||||
| 
 | ||||
|     const { getSearchText, getSearchContext } = useSearch( | ||||
|         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
 | ||||
|     const table = useReactTable( | ||||
|         withTableState(tableState, setTableState, { | ||||
|             columns, | ||||
|             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 ( | ||||
|         <> | ||||
|             <PageContent | ||||
|                 disableLoading | ||||
|                 disablePadding | ||||
|                 className={styles.container} | ||||
|                 style={{ ...paginatedStyles, ...style }} | ||||
|                 header={ | ||||
|                     <Box | ||||
|                         ref={headerLoadingRef} | ||||
|                         aria-busy={initialLoad} | ||||
|                         aria-live='polite' | ||||
|                         sx={(theme) => ({ | ||||
|                             padding: `${theme.spacing(2.5)} ${theme.spacing( | ||||
|                                 3.125, | ||||
|                             )}`,
 | ||||
|                         })} | ||||
|                     > | ||||
|                         <PageHeader | ||||
|                             titleElement={ | ||||
|                                 showTitle | ||||
|                                     ? `Feature toggles (${ | ||||
|                                           total || rows.length | ||||
|                                       })` | ||||
|                                     : null | ||||
|                             } | ||||
|                             actions={ | ||||
|                                 <> | ||||
|                                     <ConditionallyRender | ||||
|                                         condition={!isSmallScreen} | ||||
|                                         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> | ||||
|                     <ProjectFeatureTogglesHeader | ||||
|                         totalItems={total} | ||||
|                         searchQuery={tableState.query || ''} | ||||
|                         onChangeSearchQuery={(query) => | ||||
|                             setTableState({ query }) | ||||
|                         } | ||||
|                         isLoading={initialLoad} | ||||
|                         dataToExport={features} // FIXME: selected columns?
 | ||||
|                         environmentsToExport={environments.map( | ||||
|                             ({ environment }) => environment, // FIXME: visible env columns?
 | ||||
|                         )} | ||||
|                     /> | ||||
|                 } | ||||
|             > | ||||
|                 <div | ||||
| @ -648,104 +180,14 @@ export const PaginatedProjectFeatureToggles = ({ | ||||
|                     aria-busy={loading} | ||||
|                     aria-live='polite' | ||||
|                 > | ||||
|                     <SearchHighlightProvider | ||||
|                         value={getSearchText(tableState.query || '')} | ||||
|                     > | ||||
|                         <VirtualizedTable | ||||
|                             rows={rows} | ||||
|                             headerGroups={headerGroups} | ||||
|                             prepareRow={prepareRow} | ||||
|                     <SearchHighlightProvider value={tableState.query || ''}> | ||||
|                         <PaginatedTable | ||||
|                             tableInstance={table} | ||||
|                             totalItems={total} | ||||
|                         /> | ||||
|                     </SearchHighlightProvider> | ||||
| 
 | ||||
|                     <ConditionallyRender | ||||
|                         condition={rows.length === 0} | ||||
|                         show={ | ||||
|                             <ConditionallyRender | ||||
|                                 condition={(tableState.query || '')?.length > 0} | ||||
|                                 show={ | ||||
|                                     <Box sx={{ padding: theme.spacing(3) }}> | ||||
|                                         <TablePlaceholder> | ||||
|                                             No feature toggles found matching | ||||
|                                             “ | ||||
|                                             {tableState.query} | ||||
|                                             ” | ||||
|                                         </TablePlaceholder> | ||||
|                                     </Box> | ||||
|                                 } | ||||
|                                 elseShow={ | ||||
|                                     <Box sx={{ padding: theme.spacing(3) }}> | ||||
|                                         <TablePlaceholder> | ||||
|                                             No feature toggles available. Get | ||||
|                                             started by adding a new feature | ||||
|                                             toggle. | ||||
|                                         </TablePlaceholder> | ||||
|                                     </Box> | ||||
|                                 } | ||||
|                             /> | ||||
|                         } | ||||
|                     /> | ||||
|                     <FeatureStaleDialog | ||||
|                         isStale={featureStaleDialogState.stale === true} | ||||
|                         isOpen={Boolean(featureStaleDialogState.featureId)} | ||||
|                         onClose={() => { | ||||
|                             setFeatureStaleDialogState({}); | ||||
|                             onChange(); | ||||
|                         }} | ||||
|                         featureId={featureStaleDialogState.featureId || ''} | ||||
|                         projectId={projectId} | ||||
|                     /> | ||||
|                     <FeatureArchiveDialog | ||||
|                         isOpen={Boolean(featureArchiveState)} | ||||
|                         onConfirm={onChange} | ||||
|                         onClose={() => { | ||||
|                             setFeatureArchiveState(undefined); | ||||
|                         }} | ||||
|                         featureIds={[featureArchiveState || '']} | ||||
|                         projectId={projectId} | ||||
|                     /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={ | ||||
|                             Boolean(uiConfig?.flags?.featuresExportImport) && | ||||
|                             !loading | ||||
|                         } | ||||
|                         show={ | ||||
|                             <ExportDialog | ||||
|                                 showExportDialog={showExportDialog} | ||||
|                                 data={data} | ||||
|                                 onClose={() => setShowExportDialog(false)} | ||||
|                                 environments={environments.map( | ||||
|                                     ({ environment }) => environment, | ||||
|                                 )} | ||||
|                             /> | ||||
|                         } | ||||
|                     /> | ||||
|                     {featureToggleModals} | ||||
|                 </div> | ||||
|             </PageContent> | ||||
|             <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