mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +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';
|
} from '../../openapi';
|
||||||
import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure';
|
import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure';
|
||||||
import { RoleAndOwnerInfo } from './RoleAndOwnerInfo';
|
import { RoleAndOwnerInfo } from './RoleAndOwnerInfo';
|
||||||
|
import { ContentGridNoProjects } from './ContentGridNoProjects';
|
||||||
|
|
||||||
const ScreenExplanation = styled(Typography)(({ theme }) => ({
|
const ScreenExplanation = styled(Typography)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
@ -197,6 +198,8 @@ export const PersonalDashboard = () => {
|
|||||||
'seen' | 'not_seen'
|
'seen' | 'not_seen'
|
||||||
>('welcome-dialog:v1', 'not_seen');
|
>('welcome-dialog:v1', 'not_seen');
|
||||||
|
|
||||||
|
const noProjects = projects.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Typography component='h2' variant='h2'>
|
<Typography component='h2' variant='h2'>
|
||||||
@ -207,6 +210,19 @@ export const PersonalDashboard = () => {
|
|||||||
most of Unleash
|
most of Unleash
|
||||||
</ScreenExplanation>
|
</ScreenExplanation>
|
||||||
<StyledHeaderTitle>Your resources</StyledHeaderTitle>
|
<StyledHeaderTitle>Your resources</StyledHeaderTitle>
|
||||||
|
{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 }}>
|
<ContentGrid container columns={{ lg: 12, md: 1 }}>
|
||||||
<SpacedGridItem item lg={4} md={1}>
|
<SpacedGridItem item lg={4} md={1}>
|
||||||
<Typography variant='h3'>My projects</Typography>
|
<Typography variant='h3'>My projects</Typography>
|
||||||
@ -289,6 +305,7 @@ export const PersonalDashboard = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</SpacedGridItem>
|
</SpacedGridItem>
|
||||||
</ContentGrid>
|
</ContentGrid>
|
||||||
|
)}
|
||||||
<ContentGrid container columns={{ lg: 12, md: 1 }} sx={{ mt: 2 }}>
|
<ContentGrid container columns={{ lg: 12, md: 1 }} sx={{ mt: 2 }}>
|
||||||
<SpacedGridItem item lg={4} md={1}>
|
<SpacedGridItem item lg={4} md={1}>
|
||||||
<Typography variant='h3'>My feature flags</Typography>
|
<Typography variant='h3'>My feature flags</Typography>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup';
|
|
||||||
import { Badge } from 'component/common/Badge/Badge';
|
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';
|
import type { ProjectSchemaOwners } from 'openapi';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -22,29 +21,7 @@ const InfoSection = styled('div')(({ theme }) => ({
|
|||||||
gap: theme.spacing(1),
|
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) => {
|
export const RoleAndOwnerInfo = ({ roles, owners }: Props) => {
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const mappedOwners = owners.map(mapOwners(uiConfig.unleashUrl));
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
@ -57,7 +34,7 @@ export const RoleAndOwnerInfo = ({ roles, owners }: Props) => {
|
|||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<span>Project owner{owners.length > 1 ? 's' : ''}</span>
|
<span>Project owner{owners.length > 1 ? 's' : ''}</span>
|
||||||
<AvatarGroup users={mappedOwners} avatarLimit={3} />
|
<AvatarGroupFromOwners users={owners} avatarLimit={3} />
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
@ -1,46 +1,14 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import type { ProjectSchema, ProjectSchemaOwners } from 'openapi';
|
import type { ProjectSchema, ProjectSchemaOwners } from 'openapi';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import { AvatarComponent } from 'component/common/AvatarGroup/AvatarGroup';
|
||||||
AvatarGroup,
|
import { AvatarGroupFromOwners } from 'component/common/AvatarGroupFromOwners/AvatarGroupFromOwners';
|
||||||
AvatarComponent,
|
|
||||||
} from 'component/common/AvatarGroup/AvatarGroup';
|
|
||||||
|
|
||||||
export interface IProjectOwnersProps {
|
export interface IProjectOwnersProps {
|
||||||
owners?: ProjectSchema['owners'];
|
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 }) => ({
|
const StyledUserName = styled('span')(({ theme }) => ({
|
||||||
fontSize: theme.typography.body2.fontSize,
|
fontSize: theme.typography.body2.fontSize,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
@ -87,15 +55,22 @@ const StyledAvatarComponent = styled(AvatarComponent)(({ theme }) => ({
|
|||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
|
const getOwnerName = (owner?: ProjectSchemaOwners[number]) => {
|
||||||
const ownersMap = useOwnersMap();
|
switch (owner?.ownerType) {
|
||||||
const users = owners.map(ownersMap);
|
case 'user':
|
||||||
|
case 'group':
|
||||||
|
return owner.name;
|
||||||
|
default:
|
||||||
|
return 'System';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper data-testid='test'>
|
<StyledWrapper data-testid='test'>
|
||||||
<StyledContainer data-loading>
|
<StyledContainer data-loading>
|
||||||
<AvatarGroup
|
<AvatarGroupFromOwners
|
||||||
users={users}
|
users={owners}
|
||||||
avatarLimit={6}
|
avatarLimit={6}
|
||||||
AvatarComponent={StyledAvatarComponent}
|
AvatarComponent={StyledAvatarComponent}
|
||||||
/>
|
/>
|
||||||
@ -106,7 +81,7 @@ export const ProjectOwners: FC<IProjectOwnersProps> = ({ owners = [] }) => {
|
|||||||
<StyledOwnerName>
|
<StyledOwnerName>
|
||||||
<StyledHeader data-loading>Owner</StyledHeader>
|
<StyledHeader data-loading>Owner</StyledHeader>
|
||||||
<StyledUserName data-loading>
|
<StyledUserName data-loading>
|
||||||
{users[0]?.name}
|
{getOwnerName(owners[0])}
|
||||||
</StyledUserName>
|
</StyledUserName>
|
||||||
</StyledOwnerName>
|
</StyledOwnerName>
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user