mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02:00
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. <img width="1300" height="956" alt="image" src="https://github.com/user-attachments/assets/603bc9a8-21a9-4888-8804-1be23e2d63ee" /> <img width="1300" height="681" alt="image" src="https://github.com/user-attachments/assets/67f6e8e9-cedf-4a70-ba95-b9c73e8d29a8" />
This commit is contained in:
parent
837c49e4a1
commit
44650e4e2f
@ -25,7 +25,8 @@ const StyledWrapper = styled(Box, {
|
||||
}));
|
||||
|
||||
const StyledSpan = styled('span')(() => ({
|
||||
display: 'inline-block',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '100%',
|
||||
}));
|
||||
|
||||
|
@ -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<ProjectSchema & any>;
|
||||
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 <ProjectsListTable projects={projectsToRender} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledGridContainer>
|
||||
<ConditionallyRender
|
||||
condition={loading}
|
||||
show={() => (
|
||||
<>
|
||||
{loadingData.map((project: ProjectSchema) => (
|
||||
<ProjectCard
|
||||
data-loading
|
||||
createdAt={project.createdAt}
|
||||
key={project.id}
|
||||
name={project.name}
|
||||
id={project.id}
|
||||
mode={project.mode}
|
||||
memberCount={2}
|
||||
health={95}
|
||||
featureCount={4}
|
||||
owners={[
|
||||
{
|
||||
ownerType: 'user',
|
||||
name: 'Loading data',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
elseShow={() => (
|
||||
<>
|
||||
{projects.map((project) =>
|
||||
link ? (
|
||||
<StyledCardLink
|
||||
key={project.id}
|
||||
to={`/projects/${project.id}`}
|
||||
>
|
||||
<ProjectCard {...project} />
|
||||
</StyledCardLink>
|
||||
) : (
|
||||
<ProjectCard key={project.id} {...project} />
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{projectsToRender.map((project) =>
|
||||
link ? (
|
||||
<StyledCardLink
|
||||
key={project.id}
|
||||
to={`/projects/${project.id}`}
|
||||
>
|
||||
<ProjectCard data-loading {...project} />
|
||||
</StyledCardLink>
|
||||
) : (
|
||||
<ProjectCard data-loading key={project.id} {...project} />
|
||||
),
|
||||
)}
|
||||
{isOss() ? <UpgradeProjectCard /> : null}
|
||||
</StyledGridContainer>
|
||||
);
|
||||
|
@ -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 = () => {
|
||||
</ProjectsListHeader>
|
||||
<ProjectGroup
|
||||
loading={loading}
|
||||
view={state.view}
|
||||
projects={
|
||||
isOss()
|
||||
? sortedProjects
|
||||
@ -169,6 +170,7 @@ export const ProjectList = () => {
|
||||
</ProjectsListHeader>
|
||||
<ProjectGroup
|
||||
loading={loading}
|
||||
view={state.view}
|
||||
projects={otherProjects}
|
||||
/>
|
||||
</div>
|
||||
|
@ -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',
|
||||
|
@ -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 } }) => (
|
||||
<FavoriteIconCell
|
||||
value={row.original.favorite}
|
||||
onClick={() => 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 } }) => (
|
||||
<TimeAgoCell
|
||||
value={
|
||||
row.original.lastUpdatedAt || row.original.createdAt
|
||||
}
|
||||
dateFormat={formatDateYMDHMS}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Number of flags',
|
||||
accessor: 'featureCount',
|
||||
Cell: ({ value }: { value: number }) => (
|
||||
<TextCell>
|
||||
{value} flag{value === 1 ? '' : 's'}
|
||||
</TextCell>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Health',
|
||||
accessor: 'health',
|
||||
Cell: ({ value }: { value: number }) => (
|
||||
<TextCell>{value}%</TextCell>
|
||||
),
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
Header: 'Last seen',
|
||||
accessor: 'lastReportedFlagUsage',
|
||||
Cell: ({ value }: { value: Date }) => (
|
||||
<ProjectLastSeen date={value} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Owner',
|
||||
accessor: 'owners',
|
||||
Cell: ({ value }: { value: ProjectSchemaOwners }) => (
|
||||
<ProjectOwners
|
||||
owners={value?.filter(
|
||||
(owner) => owner.ownerType !== 'system',
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Members',
|
||||
accessor: 'memberCount',
|
||||
Cell: ({ value }: { value: number }) => (
|
||||
<TextCell>{value} members</TextCell>
|
||||
),
|
||||
},
|
||||
],
|
||||
[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 (
|
||||
<VirtualizedTable
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
prepareRow={prepareRow}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<StyledCellContainer>
|
||||
<ProjectModeBadge mode={row.original.mode} />
|
||||
<StyledFeatureLink to={`/projects/${row.original.id}`}>
|
||||
<Truncator title={row.original.name} lines={2} arrow>
|
||||
<Highlighter search={searchQuery}>
|
||||
{row.original.name}
|
||||
</Highlighter>
|
||||
</Truncator>
|
||||
</StyledFeatureLink>
|
||||
</StyledCellContainer>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user