From e1cddfec1dc0edc151388fb984f06c2d46355933 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:24:51 +0100 Subject: [PATCH] spike how to handle tables without react-table --- .../common/Table/cells/LinkCell/LinkCell.tsx | 4 +- .../ExperimentalProjectFeatures.tsx | 4 +- .../ExperimentalPaginatedFeatureToggles.tsx | 375 ++++++++++++++++++ .../createFeatureToggleCell.tsx | 2 +- .../ProjectFeatureTogglesHeader.tsx | 175 ++++++++ 5 files changed, 555 insertions(+), 5 deletions(-) create mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/ExperimentalPaginatedFeatureToggles.tsx create mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx diff --git a/frontend/src/component/common/Table/cells/LinkCell/LinkCell.tsx b/frontend/src/component/common/Table/cells/LinkCell/LinkCell.tsx index d23452ff3c..1ec25a9905 100644 --- a/frontend/src/component/common/Table/cells/LinkCell/LinkCell.tsx +++ b/frontend/src/component/common/Table/cells/LinkCell/LinkCell.tsx @@ -15,7 +15,7 @@ interface ILinkCellProps { title?: string; to?: string; onClick?: () => void; - subtitle?: string; + subtitle?: string | null; } export const LinkCell: FC = ({ @@ -45,7 +45,7 @@ export const LinkCell: FC = ({ <> - {subtitle} + {subtitle || ''} diff --git a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx index aa048678d1..784bb8e7b0 100644 --- a/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx +++ b/frontend/src/component/project/Project/ExperimentalProjectFeatures/ExperimentalProjectFeatures.tsx @@ -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 { ExperimentalPaginatedFeatureToggles } from '../ProjectFeatureToggles/ExperimentalPaginatedFeatureToggles'; const refreshInterval = 15 * 1000; @@ -44,7 +44,7 @@ const PaginatedProjectOverview = () => { - = ColumnProps & { + cell: (row: T) => JSX.Element; +}; + +const HeaderCell: VFC<{ + column: ColumnProps; + tableState: { + sortBy: string; + sortOrder: string; + }; + setTableState: (state: { + sortBy: string; + sortOrder: string; + }) => void; +}> = ({ column, tableState, setTableState }) => ( + { + if (column.isSortable) { + setTableState({ + sortBy: column.id, + sortOrder: tableState.sortOrder === 'desc' ? 'asc' : 'desc', + }); + } + }} + styles={{ borderRadius: '0px' }} + > + {column.header} + +); + +export const ExperimentalPaginatedFeatureToggles = ({ + environments, + style, + refreshInterval = 15 * 1000, + storageKey = 'project-feature-toggles', +}: IExperimentalPaginatedFeatureTogglesProps) => { + const projectId = useRequiredPathParam('projectId'); + const { isChangeRequestConfigured } = useChangeRequestsEnabled(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 bodyLoadingRef = useLoading(loading); + + const data = useMemo( + () => + features.map((feature) => ({ + ...feature, + archivedAt: feature.archivedAt || undefined, + createdAt: feature.createdAt || '', + lastSeenAt: feature.lastSeenAt || undefined, + type: feature.type || '', + + environments: Object.fromEntries( + environments.map((env) => { + const thisEnv = feature?.environments?.find( + (featureEnvironment) => + featureEnvironment?.name === env.environment, + ); + return [ + env, + { + 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], + ); + + type DataItem = (typeof data)[number]; + + const columns: ColumnType[] = [ + { + id: 'name', + header: 'Name', + cell: (row) => ( + + ), + isSortable: true, + }, + { + id: 'type', + header: 'Type', + cell: (row) => , + align: 'center', + isSortable: true, + }, + ...environments.map( + (environment) => + ({ + id: environment.environment, + header: environment.environment, + cell: (row: DataItem) => ( + {}} + /> + ), + align: 'center', + isSortable: true, + }) as const, + ), + ]; + + return ( + <> + + setTableState({ query }) + } + isLoading={initialLoad} + dataToExport={features} // FIXME: selected columns? + environmentsToExport={environments.map( + ({ environment }) => environment, // FIXME: visible env columns? + )} + /> + } + > +
+ + + + + {columns.map((column) => ( + + ))} + + + + {data.map((feature) => ( + + {columns.map((column) => ( + + {column.cell(feature)} + + ))} + + ))} + +
+ 0} + show={ + + setTableState({ + offset: + tableState.offset + + tableState.limit, + }) + } + fetchPrevPage={() => + setTableState({ + offset: + tableState.offset - + tableState.limit, + }) + } + setPageLimit={(pageSize) => + setTableState({ + offset: 0, + limit: pageSize, + }) + } + /> + } + /> +
+
+
+ + ); +}; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx index 0df836335e..8c1dd77318 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/createFeatureToggleCell.tsx @@ -82,7 +82,7 @@ const FeatureToggleCellComponent = ({ ); }; -const MemoizedFeatureToggleCell = React.memo(FeatureToggleCellComponent); +export const MemoizedFeatureToggleCell = React.memo(FeatureToggleCellComponent); export const createFeatureToggleCell = ( diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx new file mode 100644 index 0000000000..03d3343863 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader.tsx @@ -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[]; + 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 ( + ({ + padding: `${theme.spacing(2.5)} ${theme.spacing(3.125)}`, + })} + > + + setShowTitle(false)} + onBlur={() => setShowTitle(true)} + hasFilters + id='projectFeatureToggles' + /> + } + /> + {/* FIXME: columns menu */} + {/* setIsCustomColumns(true)} + /> */} + + + + + setShowExportDialog(true) + } + sx={(theme) => ({ + marginRight: theme.spacing(2), + })} + > + + + + + + setShowExportDialog(false) + } + environments={ + environmentsToExport || [] + } + /> + } + /> + + } + /> + + navigate(getCreateTogglePath(projectId)) + } + maxWidth='960px' + Icon={Add} + projectId={projectId} + permission={CREATE_FEATURE} + data-testid='NAVIGATE_TO_CREATE_FEATURE' + > + New feature toggle + + + } + > + + } + /> + + + ); +};