mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
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
This commit is contained in:
parent
e876e6438d
commit
5647fc916e
@ -90,13 +90,13 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({
|
||||
placement='bottom-start'
|
||||
arrow
|
||||
>
|
||||
<StyledDescription>
|
||||
<StyledDescription data-loading>
|
||||
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||
</StyledDescription>
|
||||
</HtmlTooltip>
|
||||
}
|
||||
elseShow={
|
||||
<StyledDescription>
|
||||
<StyledDescription data-loading>
|
||||
<Highlighter search={searchQuery}>{text}</Highlighter>
|
||||
</StyledDescription>
|
||||
}
|
||||
|
@ -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<IFeatureLifecycleProps> = ({
|
||||
|
@ -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<FeatureSearchResponseSchema>();
|
||||
|
||||
@ -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: () => (
|
||||
<FavoriteIconHeader
|
||||
isActive={tableState.favoritesFirst}
|
||||
onClick={() =>
|
||||
setTableState({
|
||||
favoritesFirst: !tableState.favoritesFirst,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
cell: ({ getValue, row }) => (
|
||||
<>
|
||||
<FavoriteIconCell
|
||||
value={getValue()}
|
||||
onClick={() => onFavorite(row.original)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('lastSeenAt', {
|
||||
header: 'Seen',
|
||||
cell: ({ row }) => (
|
||||
<FeatureEnvironmentSeenCell feature={row.original} />
|
||||
),
|
||||
meta: {
|
||||
align: 'center',
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('type', {
|
||||
header: 'Type',
|
||||
cell: ({ getValue }) => <FeatureTypeCell value={getValue()} />,
|
||||
meta: {
|
||||
align: 'center',
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
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}`}
|
||||
/>
|
||||
),
|
||||
meta: {
|
||||
width: '50%',
|
||||
},
|
||||
}),
|
||||
...(!flagsReleaseManagementUIEnabled
|
||||
() =>
|
||||
flagsReleaseManagementUIEnabled
|
||||
? [
|
||||
columnHelper.accessor('favorite', {
|
||||
header: () => (
|
||||
<FavoriteIconHeader
|
||||
isActive={tableState.favoritesFirst}
|
||||
onClick={() =>
|
||||
setTableState({
|
||||
favoritesFirst:
|
||||
!tableState.favoritesFirst,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
cell: ({ getValue, row }) => (
|
||||
<FavoriteIconCell
|
||||
value={getValue()}
|
||||
onClick={() => 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 }) => (
|
||||
<DateCell value={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 } }) => (
|
||||
<FeatureLifecycleCell
|
||||
feature={original}
|
||||
data-loading
|
||||
/>
|
||||
),
|
||||
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 (
|
||||
<LinkCell
|
||||
title={projectName || projectId}
|
||||
to={`/projects/${projectId}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
]
|
||||
: [
|
||||
columnHelper.accessor('favorite', {
|
||||
header: () => (
|
||||
<FavoriteIconHeader
|
||||
isActive={tableState.favoritesFirst}
|
||||
onClick={() =>
|
||||
setTableState({
|
||||
favoritesFirst:
|
||||
!tableState.favoritesFirst,
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
cell: ({ getValue, row }) => (
|
||||
<>
|
||||
<FavoriteIconCell
|
||||
value={getValue()}
|
||||
onClick={() => onFavorite(row.original)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('lastSeenAt', {
|
||||
header: 'Seen',
|
||||
cell: ({ row }) => (
|
||||
<FeatureEnvironmentSeenCell
|
||||
feature={row.original}
|
||||
/>
|
||||
),
|
||||
meta: {
|
||||
align: 'center',
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('type', {
|
||||
header: 'Type',
|
||||
cell: ({ getValue }) => (
|
||||
<FeatureTypeCell value={getValue()} />
|
||||
),
|
||||
meta: {
|
||||
align: 'center',
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
|
||||
columnHelper.accessor('name', {
|
||||
header: 'Name',
|
||||
cell: ({ row }) => (
|
||||
<LinkCell
|
||||
title={row.original.name}
|
||||
subtitle={
|
||||
row.original.description || undefined
|
||||
}
|
||||
to={`/projects/${row.original.project}/features/${row.original.name}`}
|
||||
/>
|
||||
),
|
||||
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 }) => <DateCell value={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 (
|
||||
<LinkCell
|
||||
title={
|
||||
flagsReleaseManagementUIEnabled
|
||||
? project?.name || value
|
||||
: value
|
||||
}
|
||||
to={`/projects/${getValue()}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
...(!flagsReleaseManagementUIEnabled
|
||||
? [
|
||||
columnHelper.accessor('createdAt', {
|
||||
header: 'Created',
|
||||
cell: ({ getValue }) => (
|
||||
<DateCell value={getValue()} />
|
||||
),
|
||||
meta: {
|
||||
width: '1%',
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('project', {
|
||||
header: 'Project ID',
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return (
|
||||
<LinkCell
|
||||
title={value}
|
||||
to={`/projects/${value}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
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,
|
||||
|
@ -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 = <K extends string>(
|
||||
[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],
|
||||
);
|
||||
|
@ -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}
|
||||
>
|
||||
<FeatureLifecycleStageIcon stage={currentStage} />
|
||||
|
@ -348,9 +348,9 @@ export const FeatureLifecycleTooltip: FC<{
|
||||
children: React.ReactElement<any, any>;
|
||||
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' ? (
|
||||
<StyledFooter>
|
||||
<EnvironmentsInfo stage={stage} />
|
||||
{stage.name === 'live' && (
|
||||
{stage.name === 'live' && onComplete ? (
|
||||
<LiveStageAction
|
||||
onComplete={onComplete}
|
||||
loading={loading}
|
||||
@ -409,8 +409,10 @@ export const FeatureLifecycleTooltip: FC<{
|
||||
environments={stage.environments!}
|
||||
/>
|
||||
</LiveStageAction>
|
||||
)}
|
||||
{stage.name === 'completed' && (
|
||||
) : null}
|
||||
{stage.name === 'completed' &&
|
||||
onArchive &&
|
||||
onUncomplete ? (
|
||||
<CompletedStageDescription
|
||||
environments={stage.environments!}
|
||||
onArchive={onArchive}
|
||||
@ -418,7 +420,7 @@ export const FeatureLifecycleTooltip: FC<{
|
||||
loading={loading}
|
||||
project={project}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</StyledFooter>
|
||||
) : null}
|
||||
</Box>
|
||||
|
Loading…
Reference in New Issue
Block a user