1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: create page for when you have no projects (#8285)

This adds a front end fallback screen for when you have no projects.


![image](https://github.com/user-attachments/assets/1e6e0a63-968a-43cf-84ee-9a67d9f0ca91)
This commit is contained in:
Thomas Heartman 2024-09-27 10:41:25 +02:00 committed by GitHub
parent b73c283e6c
commit 6655b2d961
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 284 additions and 144 deletions

View File

@ -0,0 +1,35 @@
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import type { ProjectSchemaOwners } from 'openapi';
import type { UserAvatar } from '../UserAvatar/UserAvatar';
import { AvatarGroup } from '../AvatarGroup/AvatarGroup';
type Props = {
users: ProjectSchemaOwners;
avatarLimit?: number;
AvatarComponent?: typeof UserAvatar;
className?: string;
};
export const AvatarGroupFromOwners: React.FC<Props> = ({ users, ...props }) => {
const { uiConfig } = useUiConfig();
const mapOwners = (owner: ProjectSchemaOwners[number]) => {
if (owner.ownerType === 'user') {
return {
name: owner.name,
imageUrl: owner.imageUrl || undefined,
email: owner.email || undefined,
};
}
if (owner.ownerType === 'group') {
return {
name: owner.name,
};
}
return {
name: 'System',
imageUrl: `${uiConfig.unleashUrl}/logo-unleash.png`,
};
};
const mappedOwners = users.map(mapOwners);
return <AvatarGroup users={mappedOwners} {...props} />;
};

View File

@ -0,0 +1,136 @@
import { Grid, Typography, styled } from '@mui/material';
import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import type { ProjectSchemaOwners } from 'openapi';
import { Link } from 'react-router-dom';
const ContentGrid = styled(Grid)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: `${theme.shape.borderRadiusLarge}px`,
}));
const SpacedGridItem = styled(Grid)(({ theme }) => ({
padding: theme.spacing(4),
border: `0.5px solid ${theme.palette.divider}`,
}));
const TitleContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
gap: theme.spacing(2),
alignItems: 'center',
fontSize: theme.spacing(1.75),
fontWeight: 'bold',
}));
const NeutralCircleContainer = styled('span')(({ theme }) => ({
width: '28px',
height: '28px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.palette.neutral.border,
borderRadius: '50%',
}));
const GridContent = styled('div')(({ theme }) => ({
flexBasis: '50%',
padding: theme.spacing(4, 2),
display: 'flex',
gap: theme.spacing(3),
flexDirection: 'column',
}));
const BoxMainContent = styled('article')(({ theme }) => ({
display: 'flex',
flexFlow: 'column',
gap: theme.spacing(2),
}));
const AdminList = styled('ul')(({ theme }) => ({
padding: 0,
'li + li': {
marginTop: theme.spacing(2),
},
}));
const AdminListItem = styled('li')(({ theme }) => ({
display: 'flex',
flexFlow: 'row',
gap: theme.spacing(2),
}));
type Props = {
owners: ProjectSchemaOwners;
admins: { name: string; imageUrl?: string }[];
};
export const ContentGridNoProjects: React.FC<Props> = ({ owners, admins }) => {
return (
<ContentGrid container columns={{ lg: 12, md: 1 }}>
<SpacedGridItem item lg={4} md={1}>
<Typography variant='h3'>My projects</Typography>
</SpacedGridItem>
<SpacedGridItem item lg={8} md={1}>
<Typography>Potential next steps</Typography>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
<GridContent>
<Typography>
You don't currently have access to any projects in the
system.
</Typography>
<Typography>
To get started, you can{' '}
<Link to='/projects?create=true'>
create your own project
</Link>
. Alternatively, you can review the available projects
in the system and ask the owner for access.
</Typography>
</GridContent>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
<GridContent>
<TitleContainer>
<NeutralCircleContainer>1</NeutralCircleContainer>
Contact Unleash admin
</TitleContainer>
<BoxMainContent>
<p>
Your Unleash administrator
{admins.length > 1 ? 's are' : ' is'}:
</p>
<AdminList>
{admins.map((admin) => (
<AdminListItem>
<UserAvatar
sx={{
margin: 0,
}}
user={admin}
/>
<Typography>{admin.name}</Typography>
</AdminListItem>
))}
</AdminList>
</BoxMainContent>
</GridContent>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
<GridContent>
<TitleContainer>
<NeutralCircleContainer>2</NeutralCircleContainer>
Ask a project owner to add you to their project
</TitleContainer>
<BoxMainContent>
<p>Project owners in Unleash:</p>
<AvatarGroupFromOwners users={owners} avatarLimit={9} />
</BoxMainContent>
</GridContent>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1} />
<SpacedGridItem item lg={8} md={1} />
</ContentGrid>
);
};

View File

@ -28,6 +28,7 @@ import type {
} from '../../openapi';
import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure';
import { RoleAndOwnerInfo } from './RoleAndOwnerInfo';
import { ContentGridNoProjects } from './ContentGridNoProjects';
const ScreenExplanation = styled(Typography)(({ theme }) => ({
marginTop: theme.spacing(1),
@ -197,6 +198,8 @@ export const PersonalDashboard = () => {
'seen' | 'not_seen'
>('welcome-dialog:v1', 'not_seen');
const noProjects = projects.length === 0;
return (
<div>
<Typography component='h2' variant='h2'>
@ -207,88 +210,102 @@ export const PersonalDashboard = () => {
most of Unleash
</ScreenExplanation>
<StyledHeaderTitle>Your resources</StyledHeaderTitle>
<ContentGrid container columns={{ lg: 12, md: 1 }}>
<SpacedGridItem item lg={4} md={1}>
<Typography variant='h3'>My projects</Typography>
</SpacedGridItem>
<SpacedGridItem
item
lg={8}
md={1}
sx={{ display: 'flex', justifyContent: 'flex-end' }}
>
<Badge color='warning'>Setup incomplete</Badge>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
<List
disablePadding={true}
sx={{ maxHeight: '400px', overflow: 'auto' }}
{noProjects ? (
<ContentGridNoProjects
owners={[{ ownerType: 'system' }]}
admins={[
{ name: 'admin' },
{
name: 'Christopher Tompkins',
imageUrl:
'https://avatars.githubusercontent.com/u/1010371?v=4',
},
]}
/>
) : (
<ContentGrid container columns={{ lg: 12, md: 1 }}>
<SpacedGridItem item lg={4} md={1}>
<Typography variant='h3'>My projects</Typography>
</SpacedGridItem>
<SpacedGridItem
item
lg={8}
md={1}
sx={{ display: 'flex', justifyContent: 'flex-end' }}
>
{projects.map((project) => {
return (
<ListItem
key={project.name}
disablePadding={true}
sx={{ mb: 1 }}
>
<ListItemButton
sx={projectStyle}
selected={
project.name === activeProject
}
onClick={() =>
setActiveProject(project.name)
}
<Badge color='warning'>Setup incomplete</Badge>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
<List
disablePadding={true}
sx={{ maxHeight: '400px', overflow: 'auto' }}
>
{projects.map((project) => {
return (
<ListItem
key={project.name}
disablePadding={true}
sx={{ mb: 1 }}
>
<ProjectBox>
<ProjectIcon color='primary' />
<StyledCardTitle>
{project.name}
</StyledCardTitle>
<IconButton
component={Link}
href={`projects/${project.name}`}
size='small'
sx={{ ml: 'auto' }}
>
<LinkIcon
titleAccess={`projects/${project.name}`}
<ListItemButton
sx={projectStyle}
selected={
project.name === activeProject
}
onClick={() =>
setActiveProject(project.name)
}
>
<ProjectBox>
<ProjectIcon color='primary' />
<StyledCardTitle>
{project.name}
</StyledCardTitle>
<IconButton
component={Link}
href={`projects/${project.name}`}
size='small'
sx={{ ml: 'auto' }}
>
<LinkIcon
titleAccess={`projects/${project.name}`}
/>
</IconButton>
</ProjectBox>
{project.name === activeProject ? (
<ActiveProjectDetails
project={project}
/>
</IconButton>
</ProjectBox>
{project.name === activeProject ? (
<ActiveProjectDetails
project={project}
/>
) : null}
</ListItemButton>
</ListItem>
);
})}
</List>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
{onboardingCompleted ? (
<ProjectSetupComplete project={activeProject} />
) : activeProject ? (
<CreateFlag project={activeProject} />
) : null}
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
{activeProject ? (
<ConnectSDK project={activeProject} />
) : null}
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1} />
<SpacedGridItem item lg={8} md={1}>
{activeProject ? (
<RoleAndOwnerInfo
roles={['owner', 'custom']}
owners={[{ ownerType: 'system' }]}
/>
) : null}
</SpacedGridItem>
</ContentGrid>
) : null}
</ListItemButton>
</ListItem>
);
})}
</List>
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
{onboardingCompleted ? (
<ProjectSetupComplete project={activeProject} />
) : activeProject ? (
<CreateFlag project={activeProject} />
) : null}
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1}>
{activeProject ? (
<ConnectSDK project={activeProject} />
) : null}
</SpacedGridItem>
<SpacedGridItem item lg={4} md={1} />
<SpacedGridItem item lg={8} md={1}>
{activeProject ? (
<RoleAndOwnerInfo
roles={['owner', 'custom']}
owners={[{ ownerType: 'system' }]}
/>
) : null}
</SpacedGridItem>
</ContentGrid>
)}
<ContentGrid container columns={{ lg: 12, md: 1 }} sx={{ mt: 2 }}>
<SpacedGridItem item lg={4} md={1}>
<Typography variant='h3'>My feature flags</Typography>

View File

@ -1,7 +1,6 @@
import { styled } from '@mui/material';
import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup';
import { Badge } from 'component/common/Badge/Badge';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners';
import type { ProjectSchemaOwners } from 'openapi';
type Props = {
@ -22,29 +21,7 @@ const InfoSection = styled('div')(({ theme }) => ({
gap: theme.spacing(1),
}));
const mapOwners =
(unleashUrl?: string) => (owner: ProjectSchemaOwners[number]) => {
if (owner.ownerType === 'user') {
return {
name: owner.name,
imageUrl: owner.imageUrl || undefined,
email: owner.email || undefined,
};
}
if (owner.ownerType === 'group') {
return {
name: owner.name,
};
}
return {
name: 'System',
imageUrl: `${unleashUrl}/logo-unleash.png`,
};
};
export const RoleAndOwnerInfo = ({ roles, owners }: Props) => {
const { uiConfig } = useUiConfig();
const mappedOwners = owners.map(mapOwners(uiConfig.unleashUrl));
return (
<Wrapper>
<InfoSection>
@ -57,7 +34,7 @@ export const RoleAndOwnerInfo = ({ roles, owners }: Props) => {
</InfoSection>
<InfoSection>
<span>Project owner{owners.length > 1 ? 's' : ''}</span>
<AvatarGroup users={mappedOwners} avatarLimit={3} />
<AvatarGroupFromOwners users={owners} avatarLimit={3} />
</InfoSection>
</Wrapper>
);

View File

@ -1,46 +1,14 @@
import type { FC } from 'react';
import { styled } from '@mui/material';
import type { ProjectSchema, ProjectSchemaOwners } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
AvatarGroup,
AvatarComponent,
} from 'component/common/AvatarGroup/AvatarGroup';
import { AvatarComponent } from 'component/common/AvatarGroup/AvatarGroup';
import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners';
export interface IProjectOwnersProps {
owners?: ProjectSchema['owners'];
}
const useOwnersMap = () => {
const { uiConfig } = useUiConfig();
return (
owner: ProjectSchemaOwners[0],
): {
name: string;
imageUrl?: string;
email?: string;
} => {
if (owner.ownerType === 'user') {
return {
name: owner.name,
imageUrl: owner.imageUrl || undefined,
email: owner.email || undefined,
};
}
if (owner.ownerType === 'group') {
return {
name: owner.name,
};
}
return {
name: 'System',
imageUrl: `${uiConfig.unleashUrl}/logo-unleash.png`,
};
};
};
const StyledUserName = styled('span')(({ theme }) => ({
fontSize: theme.typography.body2.fontSize,
lineHeight: 1,
@ -87,15 +55,22 @@ const StyledAvatarComponent = styled(AvatarComponent)(({ theme }) => ({
cursor: 'default',
}));
export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
const ownersMap = useOwnersMap();
const users = owners.map(ownersMap);
const getOwnerName = (owner?: ProjectSchemaOwners[number]) => {
switch (owner?.ownerType) {
case 'user':
case 'group':
return owner.name;
default:
return 'System';
}
};
export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
return (
<StyledWrapper data-testid='test'>
<StyledContainer data-loading>
<AvatarGroup
users={users}
<AvatarGroupFromOwners
users={owners}
avatarLimit={6}
AvatarComponent={StyledAvatarComponent}
/>
@ -106,7 +81,7 @@ export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
<StyledOwnerName>
<StyledHeader data-loading>Owner</StyledHeader>
<StyledUserName data-loading>
{users[0]?.name}
{getOwnerName(owners[0])}
</StyledUserName>
</StyledOwnerName>
}