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

feat: new onboarding welcome screen logic (#8110)

1. We will not show grid until 2 flags exist
2. Now new feature creation button will be always displayed on top with
different style
3. Moved some text around


![image](https://github.com/user-attachments/assets/6cfc2152-b52d-479c-8a2e-988c9e8b79ad)
This commit is contained in:
Jaanus Sellin 2024-09-06 13:15:28 +03:00 committed by GitHub
parent f0ba4e5180
commit b6e22d6178
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 178 additions and 122 deletions

View File

@ -4,6 +4,8 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import type { ITooltipResolverProps } from '../TooltipResolver/TooltipResolver'; import type { ITooltipResolverProps } from '../TooltipResolver/TooltipResolver';
import type { OverridableStringUnion } from '@mui/types';
import type { ButtonPropsVariantOverrides } from '@mui/material/Button/Button';
interface IResponsiveButtonProps { interface IResponsiveButtonProps {
Icon: React.ElementType; Icon: React.ElementType;
@ -15,6 +17,10 @@ interface IResponsiveButtonProps {
projectId?: string; projectId?: string;
environmentId?: string; environmentId?: string;
maxWidth: string; maxWidth: string;
variant?: OverridableStringUnion<
'text' | 'outlined' | 'contained',
ButtonPropsVariantOverrides
>;
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
} }
@ -29,6 +35,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
environmentId, environmentId,
projectId, projectId,
endIcon, endIcon,
variant,
...rest ...rest
}) => { }) => {
const smallScreen = useMediaQuery(`(max-width:${maxWidth})`); const smallScreen = useMediaQuery(`(max-width:${maxWidth})`);
@ -55,7 +62,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
permission={permission} permission={permission}
projectId={projectId} projectId={projectId}
color='primary' color='primary'
variant='contained' variant={variant}
disabled={disabled} disabled={disabled}
environmentId={environmentId} environmentId={environmentId}
endIcon={endIcon} endIcon={endIcon}

View File

@ -114,6 +114,11 @@ export const ProjectFeatureToggles = ({
const isPlaceholder = Boolean(initialLoad || (loading && total)); const isPlaceholder = Boolean(initialLoad || (loading && total));
const onboardingStarted =
onboardingUIEnabled && project.onboardingStatus.status !== 'onboarded';
const hasMultipleFeaturesOrNotOnboarding =
(total !== undefined && total > 1) || !onboardingStarted;
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.display({ columnHelper.display({
@ -399,10 +404,7 @@ export const ProjectFeatureToggles = ({
return ( return (
<Container> <Container>
<ConditionallyRender <ConditionallyRender
condition={ condition={onboardingStarted}
onboardingUIEnabled &&
project.onboardingStatus.status !== 'onboarded'
}
show={ show={
<ProjectOnboarding <ProjectOnboarding
projectId={projectId} projectId={projectId}
@ -410,111 +412,126 @@ export const ProjectFeatureToggles = ({
/> />
} }
/> />
<PageContent <ConditionallyRender
disableLoading condition={hasMultipleFeaturesOrNotOnboarding}
disablePadding show={
header={ <PageContent
<ProjectFeatureTogglesHeader disableLoading
isLoading={initialLoad} disablePadding
totalItems={total} header={
searchQuery={tableState.query || ''} <ProjectFeatureTogglesHeader
onChangeSearchQuery={(query) => { isLoading={initialLoad}
setTableState({ query }); totalItems={total}
}} searchQuery={tableState.query || ''}
dataToExport={data} onChangeSearchQuery={(query) => {
environmentsToExport={environments} setTableState({ query });
actions={ }}
<ColumnsMenu dataToExport={data}
columns={[ environmentsToExport={environments}
{ actions={
header: 'Name', <ColumnsMenu
id: 'name', columns={[
isVisible: columnVisibility.name, {
isStatic: true, header: 'Name',
}, id: 'name',
{ isVisible:
header: 'Created', columnVisibility.name,
id: 'createdAt', isStatic: true,
isVisible: columnVisibility.createdAt, },
}, {
{ header: 'Created',
header: 'By', id: 'createdAt',
id: 'createdBy', isVisible:
isVisible: columnVisibility.createdBy, columnVisibility.createdAt,
}, },
{ {
header: 'Last seen', header: 'By',
id: 'lastSeenAt', id: 'createdBy',
isVisible: columnVisibility.lastSeenAt, isVisible:
}, columnVisibility.createdBy,
{ },
header: 'Lifecycle', {
id: 'lifecycle', header: 'Last seen',
isVisible: columnVisibility.lifecycle, id: 'lastSeenAt',
}, isVisible:
{ columnVisibility.lastSeenAt,
id: 'divider', },
}, {
...environments.map((environment) => ({ header: 'Lifecycle',
header: environment, id: 'lifecycle',
id: formatEnvironmentColumnId( isVisible:
environment, columnVisibility.lifecycle,
), },
isVisible: {
columnVisibility[ id: 'divider',
formatEnvironmentColumnId( },
environment, ...environments.map(
) (environment) => ({
], header: environment,
})), id: formatEnvironmentColumnId(
]} environment,
onToggle={onToggleColumnVisibility} ),
isVisible:
columnVisibility[
formatEnvironmentColumnId(
environment,
)
],
}),
),
]}
onToggle={onToggleColumnVisibility}
/>
}
/> />
} }
/> bodyClass='noop'
style={{ cursor: 'inherit' }}
>
<div
ref={bodyLoadingRef}
aria-busy={isPlaceholder}
aria-live='polite'
>
<ProjectOverviewFilters
project={projectId}
onChange={setTableState}
state={filterState}
/>
<SearchHighlightProvider
value={tableState.query || ''}
>
<PaginatedTable
tableInstance={table}
totalItems={total}
/>
</SearchHighlightProvider>
<ConditionallyRender
condition={!data.length && !isPlaceholder}
show={
<TableEmptyState
query={tableState.query || ''}
/>
}
/>
{rowActionsDialogs}
{featureToggleModals}
</div>
</PageContent>
} }
bodyClass='noop' />
style={{ cursor: 'inherit' }} {'feature' in project.onboardingStatus ? (
> <ConnectSdkDialog
<div open={connectSdkOpen}
ref={bodyLoadingRef} onClose={() => {
aria-busy={isPlaceholder} setConnectSdkOpen(false);
aria-live='polite' }}
> project={projectId}
<ProjectOverviewFilters environments={environments}
project={projectId} feature={project.onboardingStatus.feature}
onChange={setTableState} />
state={filterState} ) : null}
/>
<SearchHighlightProvider value={tableState.query || ''}>
<PaginatedTable
tableInstance={table}
totalItems={total}
/>
</SearchHighlightProvider>
<ConditionallyRender
condition={!data.length && !isPlaceholder}
show={
<TableEmptyState query={tableState.query || ''} />
}
/>
{rowActionsDialogs}
{featureToggleModals}
{'feature' in project.onboardingStatus ? (
<ConnectSdkDialog
open={connectSdkOpen}
onClose={() => {
setConnectSdkOpen(false);
}}
project={projectId}
environments={environments}
feature={project.onboardingStatus.feature}
/>
) : null}
</div>
</PageContent>
<BatchSelectionActionsBar count={selectedData.length}> <BatchSelectionActionsBar count={selectedData.length}>
<ProjectFeaturesBatchActions <ProjectFeaturesBatchActions
selectedIds={Object.keys(rowSelection)} selectedIds={Object.keys(rowSelection)}

View File

@ -26,6 +26,8 @@ import { useFeedback } from 'component/feedbackNew/useFeedback';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { CreateFeatureDialog } from './CreateFeatureDialog'; import { CreateFeatureDialog } from './CreateFeatureDialog';
import IosShare from '@mui/icons-material/IosShare'; import IosShare from '@mui/icons-material/IosShare';
import type { OverridableStringUnion } from '@mui/types';
import type { ButtonPropsVariantOverrides } from '@mui/material/Button/Button';
interface IProjectFeatureTogglesHeaderProps { interface IProjectFeatureTogglesHeaderProps {
isLoading?: boolean; isLoading?: boolean;
@ -37,11 +39,22 @@ interface IProjectFeatureTogglesHeaderProps {
actions?: ReactNode; actions?: ReactNode;
} }
interface IFlagCreationButtonProps {
text?: string;
variant?: OverridableStringUnion<
'text' | 'outlined' | 'contained',
ButtonPropsVariantOverrides
>;
}
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
})); }));
export const FlagCreationButton: FC = () => { export const FlagCreationButton = ({
variant,
text = 'New feature flag',
}: IFlagCreationButtonProps) => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const showCreateDialog = Boolean(searchParams.get('create')); const showCreateDialog = Boolean(searchParams.get('create'));
@ -56,10 +69,11 @@ export const FlagCreationButton: FC = () => {
Icon={Add} Icon={Add}
projectId={projectId} projectId={projectId}
disabled={loading} disabled={loading}
variant={variant}
permission={CREATE_FEATURE} permission={CREATE_FEATURE}
data-testid='NAVIGATE_TO_CREATE_FEATURE' data-testid='NAVIGATE_TO_CREATE_FEATURE'
> >
New feature flag {text}
</StyledResponsiveButton> </StyledResponsiveButton>
<CreateFeatureDialog <CreateFeatureDialog
open={openCreateDialog} open={openCreateDialog}

View File

@ -1,4 +1,4 @@
import { styled, Typography, useTheme } from '@mui/material'; import { styled, Typography } from '@mui/material';
import Add from '@mui/icons-material/Add'; import Add from '@mui/icons-material/Add';
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import { FlagCreationButton } from '../ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; import { FlagCreationButton } from '../ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
@ -76,13 +76,24 @@ const TypeCircleContainer = styled(MainCircleContainer)(({ theme }) => ({
borderRadius: '20%', borderRadius: '20%',
})); }));
const StyledLink = styled(Link)({ const FlagLink = styled(Link)({
fontWeight: 'bold', fontWeight: 'bold',
textDecoration: 'none', textDecoration: 'none',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
}); });
const ExistingFlagContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(3),
height: '100%',
}));
const FlagCreationContainer = styled('div')(({ theme }) => ({
marginTop: 'auto',
}));
export const WelcomeToProject = ({ export const WelcomeToProject = ({
projectId, projectId,
setConnectSdkOpen, setConnectSdkOpen,
@ -119,7 +130,9 @@ export const WelcomeToProject = ({
Connect an SDK Connect an SDK
</TitleContainer> </TitleContainer>
<Typography> <Typography>
We have not detected any connected SDKs on this project. Your project is not yet connected to any SDK. In order
to start using your feature flag connect an SDK to the
project.
</Typography> </Typography>
<ResponsiveButton <ResponsiveButton
onClick={() => { onClick={() => {
@ -150,13 +163,12 @@ const CreateFlag = () => {
<div>The project currently holds no feature toggles.</div> <div>The project currently holds no feature toggles.</div>
<div>Create a feature flag to get started.</div> <div>Create a feature flag to get started.</div>
</Typography> </Typography>
<FlagCreationButton /> <FlagCreationButton text='Create flag' />
</> </>
); );
}; };
const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => { const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => {
const theme = useTheme();
const { feature } = useFeature(projectId, featureId); const { feature } = useFeature(projectId, featureId);
const { featureTypes } = useFeatureTypes(); const { featureTypes } = useFeatureTypes();
const IconComponent = getFeatureTypeIcons(feature.type); const IconComponent = getFeatureTypeIcons(feature.type);
@ -166,7 +178,7 @@ const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => {
const typeTitle = `${typeName || feature.type} flag`; const typeTitle = `${typeName || feature.type} flag`;
return ( return (
<> <ExistingFlagContainer>
<TitleContainer> <TitleContainer>
<MainCircleContainer></MainCircleContainer> <MainCircleContainer></MainCircleContainer>
Create a feature flag Create a feature flag
@ -177,16 +189,21 @@ const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => {
<IconComponent /> <IconComponent />
</TypeCircleContainer> </TypeCircleContainer>
</HtmlTooltip> </HtmlTooltip>
<StyledLink <FlagLink
to={`/projects/${projectId}/features/${feature.name}`} to={`/projects/${projectId}/features/${feature.name}`}
> >
{feature.name} {feature.name}
</StyledLink> </FlagLink>
<Link to={`/projects/${projectId}/features/${feature.name}`}>
view flag
</Link>
</TitleContainer> </TitleContainer>
<Typography> <FlagCreationContainer>
Your project is not yet connected to any SDK. In order to start <FlagCreationButton
using your feature flag connect an SDK to the project. variant='outlined'
</Typography> text='Create a new flag'
</> />
</FlagCreationContainer>
</ExistingFlagContainer>
); );
}; };

View File

@ -86,6 +86,7 @@ export class OnboardingReadModel implements IOnboardingReadModel {
const feature = await this.db('features') const feature = await this.db('features')
.select('name') .select('name')
.where('project', projectId) .where('project', projectId)
.where('archived_at', null)
.first(); .first();
if (!feature) { if (!feature) {