From 5647fc916e0feee324730851ef2845b39f60c3f1 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 9 Apr 2025 09:50:30 +0200 Subject: [PATCH] feat: adjust search page columns (#9722) New columns for search page - improved "name" with filtering by type and tag - lifecycle - created by (avatars) with filtering --- .../FeatureOverviewCell.tsx | 4 +- .../FeatureEnvironmentSeenCell.tsx | 6 +- .../FeatureToggleListTable.tsx | 308 ++++++++++++------ .../useGlobalFeatureSearch.ts | 56 ++++ .../FeatureLifecycle/FeatureLifecycle.tsx | 10 +- .../FeatureLifecycleTooltip.tsx | 16 +- 6 files changed, 281 insertions(+), 119 deletions(-) diff --git a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx index 16cfda8521..2a8c678cf2 100644 --- a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx +++ b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx @@ -90,13 +90,13 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ placement='bottom-start' arrow > - + {text} } elseShow={ - + {text} } diff --git a/frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell.tsx b/frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell.tsx index f7a27284e0..e3989f4863 100644 --- a/frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell.tsx +++ b/frontend/src/component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell.tsx @@ -35,9 +35,9 @@ interface IFeatureLifecycleProps { project: string; name: string; }; - onComplete: () => void; - onUncomplete: () => void; - onArchive: () => void; + onComplete?: () => void; + onUncomplete?: () => void; + onArchive?: () => void; } export const FeatureLifecycleCell: VFC = ({ diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx index 40fe440625..95751cb6c5 100644 --- a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx @@ -20,7 +20,10 @@ import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironmen import { ExportDialog } from './ExportDialog'; import { useUiFlag } from 'hooks/useUiFlag'; import { focusable } from 'themes/themeStyles'; -import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; +import { + FeatureEnvironmentSeenCell, + FeatureLifecycleCell, +} from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; import useToast from 'hooks/useToast'; import { FeatureToggleFilters } from './FeatureToggleFilters/FeatureToggleFilters'; import { withTableState } from 'utils/withTableState'; @@ -29,18 +32,36 @@ import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentC import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions'; import useLoading from 'hooks/useLoading'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; -import { useGlobalFeatureSearch } from './useGlobalFeatureSearch'; +import { + useGlobalFeatureSearch, + useTableStateFilter, +} from './useGlobalFeatureSearch'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; import { LifecycleFilters } from './FeatureToggleFilters/LifecycleFilters'; import { ExportFlags } from './ExportFlags'; +import { createFeatureOverviewCell } from 'component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell'; +import { AvatarCell } from 'component/project/Project/PaginatedProjectFeatureToggles/AvatarCell'; export const featuresPlaceholder = Array(15).fill({ name: 'Name of the feature', description: 'Short description of the feature', type: '-', - createdAt: new Date(2022, 1, 1), + createdAt: new Date(2022, 1, 1).toISOString(), project: 'projectID', -}); + createdBy: { + id: 0, + name: 'admin', + imageUrl: '', + }, + archivedAt: null, + favorite: false, + stale: false, + dependencyType: null, + tags: [], + environments: [], + impressionData: false, + segments: [], +} as FeatureSearchResponseSchema); const columnHelper = createColumnHelper(); @@ -70,6 +91,22 @@ export const FeatureToggleListTable: FC = () => { setTableState, filterState, } = useGlobalFeatureSearch(); + const onFlagTypeClick = useTableStateFilter( + ['type', 'IS'], + tableState, + setTableState, + ); + const onTagClick = useTableStateFilter( + ['tag', 'INCLUDE'], + tableState, + setTableState, + ); + const onAvatarClick = useTableStateFilter( + ['createdBy', 'IS'], + tableState, + setTableState, + ); + const { projects } = useProjects(); const bodyLoadingRef = useLoading(loading); const { favorite, unfavorite } = useFavoriteFeaturesApi(); @@ -92,65 +129,147 @@ export const FeatureToggleListTable: FC = () => { ); const columns = useMemo( - () => [ - columnHelper.accessor('favorite', { - header: () => ( - - setTableState({ - favoritesFirst: !tableState.favoritesFirst, - }) - } - /> - ), - cell: ({ getValue, row }) => ( - <> - onFavorite(row.original)} - /> - - ), - enableSorting: false, - meta: { - width: '1%', - }, - }), - columnHelper.accessor('lastSeenAt', { - header: 'Seen', - cell: ({ row }) => ( - - ), - meta: { - align: 'center', - width: '1%', - }, - }), - columnHelper.accessor('type', { - header: 'Type', - cell: ({ getValue }) => , - meta: { - align: 'center', - width: '1%', - }, - }), - columnHelper.accessor('name', { - header: 'Name', - // cell: (cell) => , - cell: ({ row }) => ( - - ), - meta: { - width: '50%', - }, - }), - ...(!flagsReleaseManagementUIEnabled + () => + flagsReleaseManagementUIEnabled ? [ + columnHelper.accessor('favorite', { + header: () => ( + + setTableState({ + favoritesFirst: + !tableState.favoritesFirst, + }) + } + /> + ), + cell: ({ getValue, row }) => ( + onFavorite(row.original)} + /> + ), + enableSorting: false, + meta: { width: 48 }, + }), + columnHelper.accessor('name', { + header: 'Name', + cell: createFeatureOverviewCell( + onTagClick, + onFlagTypeClick, + ), + meta: { width: '50%' }, + }), + columnHelper.accessor('createdAt', { + header: 'Created', + cell: ({ getValue }) => ( + + ), + meta: { width: '1%' }, + }), + columnHelper.accessor('createdBy', { + id: 'createdBy', + header: 'By', + cell: AvatarCell(onAvatarClick), + meta: { width: '1%', align: 'center' }, + enableSorting: false, + }), + + columnHelper.accessor('lifecycle', { + id: 'lifecycle', + header: 'Lifecycle', + cell: ({ row: { original } }) => ( + + ), + enableSorting: false, // FIXME: enable sorting by lifecycle + size: 50, + meta: { align: 'center', width: '1%' }, + }), + columnHelper.accessor('project', { + header: 'Project', + cell: ({ getValue }) => { + const projectId = getValue(); + const projectName = projects.find( + (project) => project.id === projectId, + )?.name; + + return ( + + ); + }, + }), + ] + : [ + columnHelper.accessor('favorite', { + header: () => ( + + setTableState({ + favoritesFirst: + !tableState.favoritesFirst, + }) + } + /> + ), + cell: ({ getValue, row }) => ( + <> + onFavorite(row.original)} + /> + + ), + enableSorting: false, + meta: { + width: '1%', + }, + }), + columnHelper.accessor('lastSeenAt', { + header: 'Seen', + cell: ({ row }) => ( + + ), + meta: { + align: 'center', + width: '1%', + }, + }), + columnHelper.accessor('type', { + header: 'Type', + cell: ({ getValue }) => ( + + ), + meta: { + align: 'center', + width: '1%', + }, + }), + + columnHelper.accessor('name', { + header: 'Name', + cell: ({ row }) => ( + + ), + meta: { + width: '50%', + }, + }), columnHelper.accessor( (row) => row.segments?.join('\n') || '', { @@ -181,42 +300,30 @@ export const FeatureToggleListTable: FC = () => { }, }, ), - ] - : ([] as never[])), - columnHelper.accessor('createdAt', { - header: 'Created', - cell: ({ getValue }) => , - meta: { - width: '1%', - }, - }), - columnHelper.accessor('project', { - header: flagsReleaseManagementUIEnabled - ? 'Project' - : 'Project ID', - cell: ({ getValue }) => { - const value = getValue(); - const project = projects.find( - (project) => project.id === value, - ); - - return ( - - ); - }, - meta: { - width: '1%', - }, - }), - ...(!flagsReleaseManagementUIEnabled - ? [ + columnHelper.accessor('createdAt', { + header: 'Created', + cell: ({ getValue }) => ( + + ), + meta: { + width: '1%', + }, + }), + columnHelper.accessor('project', { + header: 'Project ID', + cell: ({ getValue }) => { + const value = getValue(); + return ( + + ); + }, + meta: { + width: '1%', + }, + }), columnHelper.accessor('stale', { header: 'State', cell: ({ getValue }) => ( @@ -226,12 +333,9 @@ export const FeatureToggleListTable: FC = () => { width: '1%', }, }), - ] - : ([] as never[])), - ], + ], [tableState.favoritesFirst], ); - const data = useMemo( () => features?.length === 0 && loading ? featuresPlaceholder : features, diff --git a/frontend/src/component/feature/FeatureToggleList/useGlobalFeatureSearch.ts b/frontend/src/component/feature/FeatureToggleList/useGlobalFeatureSearch.ts index adb5d673bb..32b5a7cab7 100644 --- a/frontend/src/component/feature/FeatureToggleList/useGlobalFeatureSearch.ts +++ b/frontend/src/component/feature/FeatureToggleList/useGlobalFeatureSearch.ts @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { encodeQueryParams, NumberParam, @@ -32,6 +33,7 @@ export const useGlobalFeatureSearch = (pageLimit = DEFAULT_PAGE_LIMIT) => { createdAt: FilterItemParam, type: FilterItemParam, lifecycle: FilterItemParam, + createdBy: FilterItemParam, }; const [tableState, setTableState] = usePersistentTableState( `${storageKey}`, @@ -71,3 +73,57 @@ export const useGlobalFeatureSearch = (pageLimit = DEFAULT_PAGE_LIMIT) => { filterState, }; }; + +// TODO: refactor +// This is similar to `useProjectFeatureSearchActions`, but more generic +// Reuse wasn't possible because the prior one is constrained to the project hook +export const useTableStateFilter = ( + [key, operator]: [K, string], + state: + | Record< + K, + | { + operator: string; + values: string[]; + } + | undefined + | null + > + | undefined + | null, + setState: (state: { + [key: string]: { + operator: string; + values: string[]; + }; + }) => void, +) => + useCallback( + (value: string | number) => { + const currentState = state ? state[key] : undefined; + console.log({ key, operator, state: currentState, value }); + + if ( + currentState && + currentState.values.length > 0 && + !currentState.values.includes(`${value}`) + ) { + setState({ + ...state, + [key]: { + operator: currentState.operator, + values: [...currentState.values, value], + }, + }); + } else if (!currentState) { + setState({ + ...state, + [key]: { + operator: operator, + values: [value], + }, + }); + } + }, + [state, setState, key, operator], + ); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx index 4cc7f082b9..6d873cedf8 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycle.tsx @@ -19,9 +19,9 @@ export interface LifecycleFeature { } export const FeatureLifecycle: FC<{ - onArchive: () => void; - onComplete: () => void; - onUncomplete: () => void; + onArchive?: () => void; + onComplete?: () => void; + onUncomplete?: () => void; feature: LifecycleFeature; }> = ({ feature, onComplete, onUncomplete, onArchive }) => { const currentStage = populateCurrentStage(feature); @@ -30,7 +30,7 @@ export const FeatureLifecycle: FC<{ const onUncompleteHandler = async () => { await markFeatureUncompleted(feature.name, feature.project); - onUncomplete(); + onUncomplete?.(); trackEvent('feature-lifecycle', { props: { eventType: 'uncomplete', @@ -44,7 +44,7 @@ export const FeatureLifecycle: FC<{ project={feature.project} onArchive={onArchive} onComplete={onComplete} - onUncomplete={onUncompleteHandler} + onUncomplete={onUncomplete ? onUncompleteHandler : undefined} loading={loading} > diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx index 53078c7aef..a04a2ea2ee 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FeatureLifecycleTooltip.tsx @@ -348,9 +348,9 @@ export const FeatureLifecycleTooltip: FC<{ children: React.ReactElement; stage: LifecycleStage; project: string; - onArchive: () => void; - onComplete: () => void; - onUncomplete: () => void; + onArchive?: () => void; + onComplete?: () => void; + onUncomplete?: () => void; loading: boolean; }> = ({ children, @@ -399,7 +399,7 @@ export const FeatureLifecycleTooltip: FC<{ {stage.name !== 'archived' ? ( - {stage.name === 'live' && ( + {stage.name === 'live' && onComplete ? ( - )} - {stage.name === 'completed' && ( + ) : null} + {stage.name === 'completed' && + onArchive && + onUncomplete ? ( - )} + ) : null} ) : null}