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:
parent
f0ba4e5180
commit
b6e22d6178
@ -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}
|
||||||
|
@ -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)}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user