mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-13 11:17:26 +02:00
chore: overhaul project list pages (#10447)
https://linear.app/unleash/issue/2-3743/overhaul-project-list-pages This is essentially scouting work on our project list pages so we have a better baseline before proceeding with the cards/list view toggle. Includes refactoring, fixes and visual improvements ensuring better consistency and alignment with our designs. Took some liberties, so feel free to tell me I'm wrong. ### Searching <img width="1224" height="667" alt="image" src="https://github.com/user-attachments/assets/3f1bf700-7323-4c00-81db-7b57d125810b" /> ### Search results only in one of the groups <img width="1216" height="384" alt="image" src="https://github.com/user-attachments/assets/f67536e3-42de-4371-9725-c38a6fe0889a" /> ### No results found <img width="1218" height="347" alt="image" src="https://github.com/user-attachments/assets/c15c3555-1f37-473e-8a3e-8a549bd24966" /> ### Helper text <img width="334" height="114" alt="image" src="https://github.com/user-attachments/assets/c9150c9c-22c6-4f73-8989-b9cba4b52793" /> ### Title truncation with tooltip <img width="333" height="192" alt="image" src="https://github.com/user-attachments/assets/1f88d82d-67b2-4327-9301-808fef1e11ac" /> ### Archived projects <img width="1075" height="351" alt="image" src="https://github.com/user-attachments/assets/87b10618-b7c4-4351-87d3-3e678ddd20ae" />
This commit is contained in:
parent
ddd503952b
commit
0ac997e63e
@ -1,10 +1,11 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import {
|
import {
|
||||||
StyledProjectCard,
|
StyledProjectCard,
|
||||||
StyledBox,
|
StyledProjectCardTitle,
|
||||||
StyledCardTitle,
|
|
||||||
StyledProjectCardBody,
|
StyledProjectCardBody,
|
||||||
StyledActions,
|
StyledProjectCardHeader,
|
||||||
|
StyledProjectCardContent,
|
||||||
|
StyledProjectCardTitleContainer,
|
||||||
} from './ProjectCard.styles';
|
} from './ProjectCard.styles';
|
||||||
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
|
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
|
||||||
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
|
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
|
||||||
@ -12,8 +13,8 @@ import type { ProjectSchemaOwners } from 'openapi';
|
|||||||
import { formatDateYMDHM } from 'utils/formatDate';
|
import { formatDateYMDHM } from 'utils/formatDate';
|
||||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { Box, Link, styled, Tooltip } from '@mui/material';
|
import { Box, styled, Tooltip } from '@mui/material';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
DELETE_PROJECT,
|
DELETE_PROJECT,
|
||||||
UPDATE_PROJECT,
|
UPDATE_PROJECT,
|
||||||
@ -24,7 +25,12 @@ import Delete from '@mui/icons-material/Delete';
|
|||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||||
import { flexRow } from 'themes/themeStyles';
|
import { Truncator } from 'component/common/Truncator/Truncator.tsx';
|
||||||
|
|
||||||
|
const StyledActions = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
export type ProjectArchiveCardProps = {
|
export type ProjectArchiveCardProps = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -36,24 +42,6 @@ export type ProjectArchiveCardProps = {
|
|||||||
owners?: ProjectSchemaOwners;
|
owners?: ProjectSchemaOwners;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledDivHeader = styled('div')(({ theme }) => ({
|
|
||||||
...flexRow,
|
|
||||||
width: '100%',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
minHeight: theme.spacing(6),
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledTitle = styled(StyledCardTitle)(({ theme }) => ({
|
|
||||||
margin: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledContent = styled('div')(({ theme }) => ({
|
|
||||||
...flexRow,
|
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@ -69,51 +57,48 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<StyledProjectCard disabled data-testid={id}>
|
<StyledProjectCard disabled data-testid={id}>
|
||||||
<StyledProjectCardBody>
|
<StyledProjectCardBody>
|
||||||
<StyledDivHeader>
|
<StyledProjectCardHeader>
|
||||||
<StyledBox data-loading>
|
<StyledProjectCardTitleContainer data-loading>
|
||||||
<Tooltip title={`id: ${id}`} arrow>
|
<Truncator
|
||||||
<StyledTitle>
|
title={name}
|
||||||
<Highlighter search={searchQuery}>
|
arrow
|
||||||
{name}
|
component={StyledProjectCardTitle}
|
||||||
</Highlighter>
|
|
||||||
</StyledTitle>
|
|
||||||
</Tooltip>
|
|
||||||
</StyledBox>
|
|
||||||
<ProjectModeBadge mode={mode} />
|
|
||||||
</StyledDivHeader>
|
|
||||||
<StyledContent>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
archivedAt
|
|
||||||
? formatDateYMDHM(
|
|
||||||
parseISO(archivedAt as string),
|
|
||||||
locationSettings.locale,
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
arrow
|
|
||||||
placement='top'
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={(theme) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<p data-loading>
|
<Highlighter search={searchQuery}>
|
||||||
Archived:{' '}
|
{name}
|
||||||
<TimeAgo date={archivedAt} refresh={false} />
|
</Highlighter>
|
||||||
</p>
|
</Truncator>
|
||||||
</Box>
|
</StyledProjectCardTitleContainer>
|
||||||
</Tooltip>
|
<ProjectModeBadge mode={mode} />
|
||||||
<Link
|
</StyledProjectCardHeader>
|
||||||
component={RouterLink}
|
<StyledProjectCardContent>
|
||||||
to={`/archive?search=project%3A${encodeURI(id)}`}
|
{archivedAt && (
|
||||||
>
|
<div data-loading>
|
||||||
<p>View archived flags</p>
|
Archived{' '}
|
||||||
</Link>
|
<Tooltip
|
||||||
</StyledContent>
|
title={formatDateYMDHM(
|
||||||
|
parseISO(archivedAt as string),
|
||||||
|
locationSettings.locale,
|
||||||
|
)}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
<TimeAgo
|
||||||
|
date={archivedAt}
|
||||||
|
refresh={false}
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div data-loading>
|
||||||
|
<Link to={`/archive?search=project%3A${encodeURI(id)}`}>
|
||||||
|
View archived flags
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</StyledProjectCardContent>
|
||||||
</StyledProjectCardBody>
|
</StyledProjectCardBody>
|
||||||
<ProjectCardFooter id={id} disabled owners={owners}>
|
<ProjectCardFooter id={id} owners={owners}>
|
||||||
<StyledActions>
|
<StyledActions>
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
onClick={onRevive}
|
onClick={onRevive}
|
||||||
@ -121,6 +106,7 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
|||||||
permission={UPDATE_PROJECT}
|
permission={UPDATE_PROJECT}
|
||||||
tooltipProps={{ title: 'Revive project' }}
|
tooltipProps={{ title: 'Revive project' }}
|
||||||
data-testid={`revive-feature-flag-button`}
|
data-testid={`revive-feature-flag-button`}
|
||||||
|
size='small'
|
||||||
>
|
>
|
||||||
<Undo />
|
<Undo />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
||||||
@ -129,6 +115,7 @@ export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
|
|||||||
projectId={id}
|
projectId={id}
|
||||||
tooltipProps={{ title: 'Permanently delete project' }}
|
tooltipProps={{ title: 'Permanently delete project' }}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
|
size='small'
|
||||||
>
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { Card, Box } from '@mui/material';
|
import { Card, Box } from '@mui/material';
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import { flexColumn, flexRow } from 'themes/themeStyles';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
|
||||||
import { flexRow } from 'themes/themeStyles';
|
|
||||||
|
|
||||||
export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
|
export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
|
||||||
({ theme, disabled = false }) => ({
|
({ theme, disabled = false }) => ({
|
||||||
@ -16,11 +14,11 @@ export const StyledProjectCard = styled(Card)<{ disabled?: boolean }>(
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
transition: 'background-color 0.2s ease-in-out',
|
transition: 'background-color 0.2s ease-in-out',
|
||||||
backgroundColor: disabled
|
backgroundColor: theme.palette.background.default,
|
||||||
? theme.palette.neutral.light
|
|
||||||
: theme.palette.background.default,
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.neutral.light,
|
backgroundColor: disabled
|
||||||
|
? theme.palette.background.default
|
||||||
|
: theme.palette.action.hover,
|
||||||
},
|
},
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: theme.shape.borderRadiusMedium,
|
||||||
}),
|
}),
|
||||||
@ -33,63 +31,31 @@ export const StyledProjectCardBody = styled(Box)(({ theme }) => ({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledDivHeader = styled('div')(({ theme }) => ({
|
|
||||||
...flexRow,
|
|
||||||
width: '100%',
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledCardTitle = styled('h3')<{ lines?: number }>(
|
export const StyledProjectCardHeader = styled('div')(({ theme }) => ({
|
||||||
({ theme, lines = 2 }) => ({
|
gap: theme.spacing(1),
|
||||||
fontWeight: theme.typography.fontWeightRegular,
|
|
||||||
fontSize: theme.typography.body1.fontSize,
|
|
||||||
lineClamp: `${lines}`,
|
|
||||||
WebkitLineClamp: lines,
|
|
||||||
lineHeight: '1.2',
|
|
||||||
display: '-webkit-box',
|
|
||||||
boxOrient: 'vertical',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledBox = styled(Box)(() => ({
|
|
||||||
...flexRow,
|
|
||||||
marginRight: 'auto',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledEditIcon = styled(Edit)(({ theme }) => ({
|
|
||||||
color: theme.palette.neutral.main,
|
|
||||||
marginRight: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledDeleteIcon = styled(Delete)(({ theme }) => ({
|
|
||||||
color: theme.palette.neutral.main,
|
|
||||||
marginRight: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledDivInfo = styled('div')(({ theme }) => ({
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledProjectCardTitleContainer = styled('div')(({ theme }) => ({
|
||||||
|
...flexColumn,
|
||||||
|
margin: theme.spacing(1, 'auto', 1, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledProjectCardTitle = styled('h3')(({ theme }) => ({
|
||||||
|
margin: 0,
|
||||||
|
marginRight: 'auto',
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
fontSize: theme.typography.body1.fontSize,
|
||||||
|
lineHeight: '1.2',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledProjectCardContent = styled('div')(({ theme }) => ({
|
||||||
|
...flexRow,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
padding: theme.spacing(0, 1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledParagraphInfo = styled('p')<{ disabled?: boolean }>(
|
|
||||||
({ theme, disabled = false }) => ({
|
|
||||||
color: disabled ? 'inherit' : theme.palette.primary.dark,
|
|
||||||
fontWeight: disabled ? 'normal' : 'bold',
|
|
||||||
fontSize: theme.typography.body1.fontSize,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledActions = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
margin: theme.spacing(0.5),
|
|
||||||
}));
|
}));
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
|
StyledProjectCardTitle,
|
||||||
StyledProjectCard,
|
StyledProjectCard,
|
||||||
StyledCardTitle,
|
|
||||||
StyledProjectCardBody,
|
StyledProjectCardBody,
|
||||||
|
StyledProjectCardHeader,
|
||||||
|
StyledProjectCardContent,
|
||||||
|
StyledProjectCardTitleContainer,
|
||||||
} from './ProjectCard.styles';
|
} from './ProjectCard.styles';
|
||||||
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
|
import { ProjectCardFooter } from './ProjectCardFooter/ProjectCardFooter.tsx';
|
||||||
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
|
import { ProjectModeBadge } from './ProjectModeBadge/ProjectModeBadge.tsx';
|
||||||
import { FavoriteAction } from './FavoriteAction/FavoriteAction.tsx';
|
import { FavoriteAction } from './FavoriteAction/FavoriteAction.tsx';
|
||||||
import { Box, styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { flexColumn, flexRow } from 'themes/themeStyles';
|
|
||||||
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
import { TimeAgo } from 'component/common/TimeAgo/TimeAgo';
|
||||||
import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen.tsx';
|
import { ProjectLastSeen } from './ProjectLastSeen/ProjectLastSeen.tsx';
|
||||||
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
import { Highlighter } from 'component/common/Highlighter/Highlighter';
|
||||||
@ -16,41 +18,18 @@ import { ProjectMembers } from './ProjectCardFooter/ProjectMembers/ProjectMember
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
import { DEFAULT_PROJECT_ID } from 'hooks/api/getters/useDefaultProject/useDefaultProjectId';
|
||||||
import type { ProjectSchema } from 'openapi';
|
import type { ProjectSchema } from 'openapi';
|
||||||
|
import { Truncator } from 'component/common/Truncator/Truncator.tsx';
|
||||||
|
|
||||||
const StyledUpdated = styled('span')(({ theme }) => ({
|
const StyledSubtitle = styled('span')(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledCount = styled('strong')(({ theme }) => ({
|
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledInfo = styled('div')(({ theme }) => ({
|
|
||||||
...flexColumn,
|
|
||||||
fontSize: theme.fontSizes.smallerBody,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledContent = styled('div')({
|
|
||||||
...flexRow,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledHeader = styled('div')(({ theme }) => ({
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'center',
|
|
||||||
}));
|
|
||||||
|
|
||||||
type ProjectCardProps = ProjectSchema & { onHover?: () => void };
|
|
||||||
|
|
||||||
export const ProjectCard = ({
|
export const ProjectCard = ({
|
||||||
name,
|
name,
|
||||||
featureCount,
|
featureCount,
|
||||||
health,
|
health,
|
||||||
memberCount = 0,
|
memberCount = 0,
|
||||||
onHover,
|
|
||||||
id,
|
id,
|
||||||
mode,
|
mode,
|
||||||
favorite = false,
|
favorite = false,
|
||||||
@ -58,47 +37,47 @@ export const ProjectCard = ({
|
|||||||
createdAt,
|
createdAt,
|
||||||
lastUpdatedAt,
|
lastUpdatedAt,
|
||||||
lastReportedFlagUsage,
|
lastReportedFlagUsage,
|
||||||
}: ProjectCardProps) => {
|
}: ProjectSchema) => {
|
||||||
const { searchQuery } = useSearchHighlightContext();
|
const { searchQuery } = useSearchHighlightContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledProjectCard onMouseEnter={onHover}>
|
<StyledProjectCard>
|
||||||
<StyledProjectCardBody>
|
<StyledProjectCardBody>
|
||||||
<StyledHeader>
|
<StyledProjectCardHeader>
|
||||||
<Box
|
<StyledProjectCardTitleContainer data-loading>
|
||||||
data-loading
|
<Truncator
|
||||||
sx={(theme) => ({
|
title={name}
|
||||||
...flexColumn,
|
arrow
|
||||||
margin: theme.spacing(1, 'auto', 1, 0),
|
component={StyledProjectCardTitle}
|
||||||
})}
|
>
|
||||||
>
|
|
||||||
<StyledCardTitle lines={1} sx={{ margin: 0 }}>
|
|
||||||
<Highlighter search={searchQuery}>
|
<Highlighter search={searchQuery}>
|
||||||
{name}
|
{name}
|
||||||
</Highlighter>
|
</Highlighter>
|
||||||
</StyledCardTitle>
|
</Truncator>
|
||||||
<StyledUpdated>
|
<StyledSubtitle>
|
||||||
Updated{' '}
|
Updated{' '}
|
||||||
<TimeAgo date={lastUpdatedAt || createdAt} />
|
<TimeAgo date={lastUpdatedAt || createdAt} />
|
||||||
</StyledUpdated>
|
</StyledSubtitle>
|
||||||
</Box>
|
</StyledProjectCardTitleContainer>
|
||||||
<ProjectModeBadge mode={mode} />
|
<ProjectModeBadge mode={mode} />
|
||||||
<FavoriteAction id={id} isFavorite={favorite} />
|
<FavoriteAction id={id} isFavorite={favorite} />
|
||||||
</StyledHeader>
|
</StyledProjectCardHeader>
|
||||||
<StyledInfo>
|
<div>
|
||||||
<div data-loading>
|
<StyledProjectCardContent>
|
||||||
<StyledCount>{featureCount}</StyledCount> flag
|
|
||||||
{featureCount === 1 ? '' : 's'}
|
|
||||||
</div>
|
|
||||||
<StyledContent>
|
|
||||||
<div data-loading>
|
<div data-loading>
|
||||||
<StyledCount>{health}%</StyledCount> health
|
<strong>{featureCount}</strong> flag
|
||||||
|
{featureCount === 1 ? '' : 's'}
|
||||||
|
</div>
|
||||||
|
</StyledProjectCardContent>
|
||||||
|
<StyledProjectCardContent>
|
||||||
|
<div data-loading>
|
||||||
|
<strong>{health}%</strong> health
|
||||||
</div>
|
</div>
|
||||||
<div data-loading>
|
<div data-loading>
|
||||||
<ProjectLastSeen date={lastReportedFlagUsage} />
|
<ProjectLastSeen date={lastReportedFlagUsage} />
|
||||||
</div>
|
</div>
|
||||||
</StyledContent>
|
</StyledProjectCardContent>
|
||||||
</StyledInfo>
|
</div>
|
||||||
</StyledProjectCardBody>
|
</StyledProjectCardBody>
|
||||||
<ProjectCardFooter id={id} owners={owners}>
|
<ProjectCardFooter id={id} owners={owners}>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
@ -11,35 +11,29 @@ interface IProjectCardFooterProps {
|
|||||||
id?: string;
|
id?: string;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
disabled?: boolean;
|
|
||||||
owners?: IProjectOwnersProps['owners'];
|
owners?: IProjectOwnersProps['owners'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledFooter = styled(Box)<{ disabled: boolean }>(
|
const StyledFooter = styled(Box)(({ theme }) => ({
|
||||||
({ theme, disabled }) => ({
|
display: 'flex',
|
||||||
display: 'flex',
|
background: theme.palette.background.elevation1,
|
||||||
background: disabled
|
boxShadow: theme.boxShadows.accordionFooter,
|
||||||
? theme.palette.background.paper
|
alignItems: 'center',
|
||||||
: theme.palette.background.elevation1,
|
justifyContent: 'space-between',
|
||||||
boxShadow: theme.boxShadows.accordionFooter,
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
alignItems: 'center',
|
paddingInline: theme.spacing(2),
|
||||||
justifyContent: 'space-between',
|
paddingBlock: theme.spacing(1.5),
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
}));
|
||||||
paddingInline: theme.spacing(2),
|
|
||||||
paddingBlock: theme.spacing(1.5),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
export const ProjectCardFooter: FC<IProjectCardFooterProps> = ({
|
||||||
children,
|
children,
|
||||||
owners,
|
owners,
|
||||||
disabled = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
const ownersWithoutSystem = owners?.filter(
|
const ownersWithoutSystem = owners?.filter(
|
||||||
(owner) => owner.ownerType !== 'system',
|
(owner) => owner.ownerType !== 'system',
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<StyledFooter disabled={disabled}>
|
<StyledFooter>
|
||||||
{ownersWithoutSystem ? (
|
{ownersWithoutSystem ? (
|
||||||
<ProjectOwners
|
<ProjectOwners
|
||||||
owners={ownersWithoutSystem as ProjectSchemaOwners}
|
owners={ownersWithoutSystem as ProjectSchemaOwners}
|
||||||
|
@ -4,9 +4,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { ProjectCard as DefaultProjectCard } from '../ProjectCard/ProjectCard.tsx';
|
import { ProjectCard as DefaultProjectCard } from '../ProjectCard/ProjectCard.tsx';
|
||||||
import type { ProjectSchema } from 'openapi';
|
import type { ProjectSchema } from 'openapi';
|
||||||
import loadingData from './loadingData.ts';
|
import loadingData from './loadingData.ts';
|
||||||
import { TablePlaceholder } from 'component/common/Table';
|
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
|
||||||
import { UpgradeProjectCard } from '../ProjectCard/UpgradeProjectCard.tsx';
|
import { UpgradeProjectCard } from '../ProjectCard/UpgradeProjectCard.tsx';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
@ -40,86 +38,57 @@ type ProjectGroupProps = {
|
|||||||
export const ProjectGroup = ({
|
export const ProjectGroup = ({
|
||||||
projects,
|
projects,
|
||||||
loading,
|
loading,
|
||||||
placeholder = 'No projects available.',
|
|
||||||
ProjectCardComponent,
|
ProjectCardComponent,
|
||||||
link = true,
|
link = true,
|
||||||
}: ProjectGroupProps) => {
|
}: ProjectGroupProps) => {
|
||||||
const ProjectCard = ProjectCardComponent ?? DefaultProjectCard;
|
const ProjectCard = ProjectCardComponent ?? DefaultProjectCard;
|
||||||
const { isOss } = useUiConfig();
|
const { isOss } = useUiConfig();
|
||||||
const { searchQuery } = useSearchHighlightContext();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledGridContainer>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={projects.length < 1 && !loading}
|
condition={loading}
|
||||||
show={
|
show={() => (
|
||||||
<ConditionallyRender
|
<>
|
||||||
condition={searchQuery?.length > 0}
|
{loadingData.map((project: ProjectSchema) => (
|
||||||
show={
|
<ProjectCard
|
||||||
<TablePlaceholder>
|
data-loading
|
||||||
No projects found matching “
|
createdAt={project.createdAt}
|
||||||
{searchQuery}
|
key={project.id}
|
||||||
”
|
name={project.name}
|
||||||
</TablePlaceholder>
|
id={project.id}
|
||||||
}
|
mode={project.mode}
|
||||||
elseShow={
|
memberCount={2}
|
||||||
<TablePlaceholder>{placeholder}</TablePlaceholder>
|
health={95}
|
||||||
}
|
featureCount={4}
|
||||||
/>
|
owners={[
|
||||||
}
|
{
|
||||||
elseShow={
|
ownerType: 'user',
|
||||||
<StyledGridContainer>
|
name: 'Loading data',
|
||||||
<ConditionallyRender
|
},
|
||||||
condition={loading}
|
]}
|
||||||
show={() => (
|
/>
|
||||||
<>
|
))}
|
||||||
{loadingData.map(
|
</>
|
||||||
(project: ProjectSchema) => (
|
)}
|
||||||
<ProjectCard
|
elseShow={() => (
|
||||||
data-loading
|
<>
|
||||||
createdAt={project.createdAt}
|
{projects.map((project) =>
|
||||||
key={project.id}
|
link ? (
|
||||||
name={project.name}
|
<StyledCardLink
|
||||||
id={project.id}
|
key={project.id}
|
||||||
mode={project.mode}
|
to={`/projects/${project.id}`}
|
||||||
memberCount={2}
|
>
|
||||||
health={95}
|
<ProjectCard {...project} />
|
||||||
featureCount={4}
|
</StyledCardLink>
|
||||||
owners={[
|
) : (
|
||||||
{
|
<ProjectCard key={project.id} {...project} />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{isOss() ? <UpgradeProjectCard /> : null}
|
|
||||||
</StyledGridContainer>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
{isOss() ? <UpgradeProjectCard /> : null}
|
||||||
|
</StyledGridContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,7 @@ import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort.ts';
|
|||||||
import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink.tsx';
|
import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink.tsx';
|
||||||
import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx';
|
import { ProjectsListHeader } from './ProjectsListHeader/ProjectsListHeader.tsx';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { TablePlaceholder } from 'component/common/Table/index.ts';
|
||||||
|
|
||||||
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
||||||
maxWidth: '500px',
|
maxWidth: '500px',
|
||||||
@ -38,7 +39,7 @@ export const ProjectList = () => {
|
|||||||
|
|
||||||
const [state, setState] = useProjectsListState();
|
const [state, setState] = useProjectsListState();
|
||||||
|
|
||||||
const myProjects = new Set(useProfile().profile?.projects || []);
|
const myProfileProjects = new Set(useProfile().profile?.projects || []);
|
||||||
|
|
||||||
const setSearchValue = useCallback(
|
const setSearchValue = useCallback(
|
||||||
(value: string) => setState({ query: value || undefined }),
|
(value: string) => setState({ query: value || undefined }),
|
||||||
@ -50,13 +51,20 @@ export const ProjectList = () => {
|
|||||||
state.query,
|
state.query,
|
||||||
state.sortBy,
|
state.sortBy,
|
||||||
);
|
);
|
||||||
const groupedProjects = useGroupedProjects(sortedProjects, myProjects);
|
const groupedProjects = useGroupedProjects(
|
||||||
|
sortedProjects,
|
||||||
|
myProfileProjects,
|
||||||
|
);
|
||||||
|
|
||||||
const projectCount =
|
const projectCount =
|
||||||
sortedProjects.length < projects.length
|
sortedProjects.length < projects.length
|
||||||
? `${sortedProjects.length} of ${projects.length}`
|
? `${sortedProjects.length} of ${projects.length}`
|
||||||
: projects.length;
|
: projects.length;
|
||||||
|
|
||||||
|
const myProjects = isOss() ? sortedProjects : groupedProjects.myProjects;
|
||||||
|
|
||||||
|
const otherProjects = isOss() ? [] : groupedProjects.otherProjects;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
@ -66,7 +74,7 @@ export const ProjectList = () => {
|
|||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!isOss && !isSmallScreen}
|
condition={!isOss() && !isSmallScreen}
|
||||||
show={
|
show={
|
||||||
<>
|
<>
|
||||||
<Search
|
<Search
|
||||||
@ -113,42 +121,61 @@ export const ProjectList = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SearchHighlightProvider value={state.query || ''}>
|
<SearchHighlightProvider value={state.query || ''}>
|
||||||
<div>
|
{myProjects.length > 0 && (
|
||||||
<ProjectsListHeader
|
|
||||||
subtitle='Favorite projects, projects you own, and projects you are a member of'
|
|
||||||
actions={
|
|
||||||
<ProjectsListSort
|
|
||||||
sortBy={state.sortBy}
|
|
||||||
setSortBy={(sortBy) =>
|
|
||||||
setState({
|
|
||||||
sortBy: sortBy as typeof state.sortBy,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
My projects
|
|
||||||
</ProjectsListHeader>
|
|
||||||
<ProjectGroup
|
|
||||||
loading={loading}
|
|
||||||
projects={
|
|
||||||
isOss()
|
|
||||||
? sortedProjects
|
|
||||||
: groupedProjects.myProjects
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!isOss() ? (
|
|
||||||
<div>
|
<div>
|
||||||
<ProjectsListHeader subtitle='Projects in Unleash that you have access to.'>
|
<ProjectsListHeader
|
||||||
|
helpText='Favorite projects, projects you own, and projects you are a member of'
|
||||||
|
actions={
|
||||||
|
<ProjectsListSort
|
||||||
|
sortBy={state.sortBy}
|
||||||
|
setSortBy={(sortBy) =>
|
||||||
|
setState({
|
||||||
|
sortBy: sortBy as typeof state.sortBy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
My projects
|
||||||
|
</ProjectsListHeader>
|
||||||
|
<ProjectGroup
|
||||||
|
loading={loading}
|
||||||
|
projects={
|
||||||
|
isOss()
|
||||||
|
? sortedProjects
|
||||||
|
: groupedProjects.myProjects
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{otherProjects.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<ProjectsListHeader helpText='Projects in Unleash that you have access to.'>
|
||||||
Other projects
|
Other projects
|
||||||
</ProjectsListHeader>
|
</ProjectsListHeader>
|
||||||
<ProjectGroup
|
<ProjectGroup
|
||||||
loading={loading}
|
loading={loading}
|
||||||
projects={groupedProjects.otherProjects}
|
projects={otherProjects}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
|
{!loading &&
|
||||||
|
!myProjects.length &&
|
||||||
|
!otherProjects.length && (
|
||||||
|
<>
|
||||||
|
{state.query?.length ? (
|
||||||
|
<TablePlaceholder>
|
||||||
|
No projects found matching “
|
||||||
|
{state.query}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
) : (
|
||||||
|
<TablePlaceholder>
|
||||||
|
No projects available.
|
||||||
|
</TablePlaceholder>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SearchHighlightProvider>
|
</SearchHighlightProvider>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { styled, Typography } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
type ProjectsListHeaderProps = {
|
type ProjectsListHeaderProps = {
|
||||||
children?: ReactNode;
|
children: ReactNode;
|
||||||
subtitle?: string;
|
helpText: string;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,28 +19,22 @@ const StyledHeaderContainer = styled('div')(({ theme }) => ({
|
|||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledHeaderTitle = styled('div')(() => ({
|
const StyledHeaderTitle = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(1),
|
||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({
|
export const ProjectsListHeader: FC<ProjectsListHeaderProps> = ({
|
||||||
children,
|
children,
|
||||||
subtitle,
|
helpText,
|
||||||
actions,
|
actions,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledHeaderContainer>
|
<StyledHeaderContainer>
|
||||||
<StyledHeaderTitle>
|
<StyledHeaderTitle>
|
||||||
{children ? (
|
{children}
|
||||||
<Typography component='h2' variant='h2'>
|
<HelpIcon tooltip={helpText} />
|
||||||
{children}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
{subtitle ? (
|
|
||||||
<Typography variant='body2' color='text.secondary'>
|
|
||||||
{subtitle}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</StyledHeaderTitle>
|
</StyledHeaderTitle>
|
||||||
{actions}
|
{actions}
|
||||||
</StyledHeaderContainer>
|
</StyledHeaderContainer>
|
||||||
|
@ -9,7 +9,7 @@ const StyledWrapper = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledContainer = styled('div')(() => ({
|
const StyledContainer = styled('div')(() => ({
|
||||||
maxWidth: '220px',
|
maxWidth: '200px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user