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:
parent
b73c283e6c
commit
6655b2d961
@ -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} />;
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user