From 44650e4e2f9c25e31e4be665b67e3d6b7c2d7165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Wed, 6 Aug 2025 11:36:21 +0100 Subject: [PATCH] chore: project list table view (#10466) https://linear.app/unleash/issue/2-3740/implement-the-project-list-view Implements the list (table) view of the projects page. image image --- .../common/Table/cells/TextCell/TextCell.tsx | 3 +- .../project/ProjectList/ProjectGroup.tsx | 67 +++------ .../project/ProjectList/ProjectList.tsx | 4 +- .../ProjectsListHeader/ProjectsListHeader.tsx | 1 + .../ProjectsListTable/ProjectsListTable.tsx | 136 ++++++++++++++++++ .../ProjectsListTableProjectName.tsx | 46 ++++++ .../project/ProjectList/loadingData.ts | 10 +- 7 files changed, 218 insertions(+), 49 deletions(-) create mode 100644 frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTable.tsx create mode 100644 frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTableProjectName.tsx diff --git a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx index 98fd97fe2a..f1581b3078 100644 --- a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx +++ b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx @@ -25,7 +25,8 @@ const StyledWrapper = styled(Box, { })); const StyledSpan = styled('span')(() => ({ - display: 'inline-block', + display: 'inline-flex', + flexDirection: 'column', maxWidth: '100%', })); diff --git a/frontend/src/component/project/ProjectList/ProjectGroup.tsx b/frontend/src/component/project/ProjectList/ProjectGroup.tsx index c0bb49e31d..11b3651d1a 100644 --- a/frontend/src/component/project/ProjectList/ProjectGroup.tsx +++ b/frontend/src/component/project/ProjectList/ProjectGroup.tsx @@ -1,12 +1,13 @@ import type { ComponentType, ReactNode } from 'react'; import { Link } from 'react-router-dom'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ProjectCard as DefaultProjectCard } from '../ProjectCard/ProjectCard.tsx'; import type { ProjectSchema } from 'openapi'; -import loadingData from './loadingData.ts'; +import { loadingData } from './loadingData.ts'; import { styled } from '@mui/material'; import { UpgradeProjectCard } from '../ProjectCard/UpgradeProjectCard.tsx'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import type { ProjectsListView } from './hooks/useProjectsListState.ts'; +import { ProjectsListTable } from './ProjectsListTable/ProjectsListTable.tsx'; const StyledGridContainer = styled('div')(({ theme }) => ({ display: 'grid', @@ -33,6 +34,7 @@ type ProjectGroupProps = { placeholder?: string; ProjectCardComponent?: ComponentType; link?: boolean; + view?: ProjectsListView; }; export const ProjectGroup = ({ @@ -40,54 +42,31 @@ export const ProjectGroup = ({ loading, ProjectCardComponent, link = true, + view = 'cards', }: ProjectGroupProps) => { const ProjectCard = ProjectCardComponent ?? DefaultProjectCard; const { isOss } = useUiConfig(); + const projectsToRender = loading ? loadingData : projects; + + if (!isOss() && view === 'list') { + return ; + } + return ( - ( - <> - {loadingData.map((project: ProjectSchema) => ( - - ))} - - )} - elseShow={() => ( - <> - {projects.map((project) => - link ? ( - - - - ) : ( - - ), - )} - - )} - /> + {projectsToRender.map((project) => + link ? ( + + + + ) : ( + + ), + )} {isOss() ? : null} ); diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index 91402773a2..33ddfaa8a7 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -30,7 +30,7 @@ const StyledApiError = styled(ApiError)(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({ display: 'flex', flexDirection: 'column', - gap: theme.spacing(6), + gap: theme.spacing(4), })); export const ProjectList = () => { @@ -154,6 +154,7 @@ export const ProjectList = () => { { diff --git a/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx b/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx index c24992ab7b..248c78eceb 100644 --- a/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx +++ b/frontend/src/component/project/ProjectList/ProjectsListHeader/ProjectsListHeader.tsx @@ -11,6 +11,7 @@ type ProjectsListHeaderProps = { const StyledHeaderContainer = styled('div')(({ theme }) => ({ display: 'flex', flexDirection: 'column-reverse', + minHeight: theme.spacing(5), gap: theme.spacing(2), [theme.breakpoints.up('md')]: { flexDirection: 'row', diff --git a/frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTable.tsx b/frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTable.tsx new file mode 100644 index 0000000000..4424b32d2b --- /dev/null +++ b/frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTable.tsx @@ -0,0 +1,136 @@ +import { VirtualizedTable } from 'component/common/Table'; +import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; +import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; +import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; +import { ProjectOwners } from 'component/project/ProjectCard/ProjectCardFooter/ProjectOwners/ProjectOwners'; +import { ProjectLastSeen } from 'component/project/ProjectCard/ProjectLastSeen/ProjectLastSeen'; +import { useFavoriteProjectsApi } from 'hooks/api/actions/useFavoriteProjectsApi/useFavoriteProjectsApi'; +import useProjects from 'hooks/api/getters/useProjects/useProjects'; +import type { ProjectSchema, ProjectSchemaOwners } from 'openapi'; +import { useCallback, useMemo } from 'react'; +import { useFlexLayout, useTable } from 'react-table'; +import { formatDateYMDHMS } from 'utils/formatDate'; +import { ProjectsListTableProjectName } from './ProjectsListTableProjectName.tsx'; + +type ProjectsListTableProps = { + projects: ProjectSchema[]; +}; + +export const ProjectsListTable = ({ projects }: ProjectsListTableProps) => { + const { refetch } = useProjects(); + const { favorite, unfavorite } = useFavoriteProjectsApi(); + + const onFavorite = useCallback( + async (project: ProjectSchema) => { + if (project?.favorite) { + await unfavorite(project.id); + } else { + await favorite(project.id); + } + refetch(); + }, + [refetch], + ); + + const columns = useMemo( + () => [ + { + Header: '', + accessor: 'favorite', + Cell: ({ row }: { row: { original: ProjectSchema } }) => ( + onFavorite(row.original)} + /> + ), + width: 40, + }, + { + Header: 'Project name', + accessor: 'name', + minWidth: 200, + searchable: true, + Cell: ProjectsListTableProjectName, + }, + { + Header: 'Last updated', + id: 'lastUpdatedAt', + Cell: ({ row }: { row: { original: ProjectSchema } }) => ( + + ), + }, + { + Header: 'Number of flags', + accessor: 'featureCount', + Cell: ({ value }: { value: number }) => ( + + {value} flag{value === 1 ? '' : 's'} + + ), + }, + { + Header: 'Health', + accessor: 'health', + Cell: ({ value }: { value: number }) => ( + {value}% + ), + width: 100, + }, + { + Header: 'Last seen', + accessor: 'lastReportedFlagUsage', + Cell: ({ value }: { value: Date }) => ( + + ), + }, + { + Header: 'Owner', + accessor: 'owners', + Cell: ({ value }: { value: ProjectSchemaOwners }) => ( + owner.ownerType !== 'system', + )} + /> + ), + }, + { + Header: 'Members', + accessor: 'memberCount', + Cell: ({ value }: { value: number }) => ( + {value} members + ), + }, + ], + [onFavorite], + ); + + const { headerGroups, rows, prepareRow } = useTable( + { + columns: columns as any, + data: projects, + autoResetHiddenColumns: false, + autoResetSortBy: false, + disableSortRemove: true, + disableMultiSort: true, + defaultColumn: { + Cell: HighlightCell, + }, + }, + useFlexLayout, + ); + + return ( + + ); +}; diff --git a/frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTableProjectName.tsx b/frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTableProjectName.tsx new file mode 100644 index 0000000000..f2b026fe87 --- /dev/null +++ b/frontend/src/component/project/ProjectList/ProjectsListTable/ProjectsListTableProjectName.tsx @@ -0,0 +1,46 @@ +import { styled } from '@mui/material'; +import { Highlighter } from 'component/common/Highlighter/Highlighter'; +import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { Truncator } from 'component/common/Truncator/Truncator'; +import { ProjectModeBadge } from 'component/project/ProjectCard/ProjectModeBadge/ProjectModeBadge'; +import type { ProjectSchema } from 'openapi'; +import { Link } from 'react-router-dom'; + +const StyledCellContainer = styled('div')(({ theme }) => ({ + display: 'inline-flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: theme.spacing(1, 2), +})); + +const StyledFeatureLink = styled(Link)({ + textDecoration: 'none', + '&:hover, &:focus': { + textDecoration: 'underline', + }, +}); + +type ProjectsListTableProjectNameProps = { + row: { + original: ProjectSchema; + }; +}; + +export const ProjectsListTableProjectName = ({ + row, +}: ProjectsListTableProjectNameProps) => { + const { searchQuery } = useSearchHighlightContext(); + + return ( + + + + + + {row.original.name} + + + + + ); +}; diff --git a/frontend/src/component/project/ProjectList/loadingData.ts b/frontend/src/component/project/ProjectList/loadingData.ts index 615facd815..f75fae9dde 100644 --- a/frontend/src/component/project/ProjectList/loadingData.ts +++ b/frontend/src/component/project/ProjectList/loadingData.ts @@ -1,4 +1,6 @@ -const loadingData = [ +import type { ProjectSchema } from 'openapi'; + +export const loadingData: ProjectSchema[] = [ { id: 'loading1', name: 'loading1', @@ -8,6 +10,7 @@ const loadingData = [ createdAt: '', description: '', mode: 'open' as const, + owners: [{ ownerType: 'user', name: 'Loading data' }], }, { id: 'loading2', @@ -18,6 +21,7 @@ const loadingData = [ createdAt: '', description: '', mode: 'open' as const, + owners: [{ ownerType: 'user', name: 'Loading data' }], }, { id: 'loading3', @@ -28,6 +32,7 @@ const loadingData = [ createdAt: '', description: '', mode: 'open' as const, + owners: [{ ownerType: 'user', name: 'Loading data' }], }, { id: 'loading4', @@ -38,7 +43,6 @@ const loadingData = [ createdAt: '', description: '', mode: 'open' as const, + owners: [{ ownerType: 'user', name: 'Loading data' }], }, ]; - -export default loadingData;