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 PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import type { ITooltipResolverProps } from '../TooltipResolver/TooltipResolver';
|
||||
import type { OverridableStringUnion } from '@mui/types';
|
||||
import type { ButtonPropsVariantOverrides } from '@mui/material/Button/Button';
|
||||
|
||||
interface IResponsiveButtonProps {
|
||||
Icon: React.ElementType;
|
||||
@ -15,6 +17,10 @@ interface IResponsiveButtonProps {
|
||||
projectId?: string;
|
||||
environmentId?: string;
|
||||
maxWidth: string;
|
||||
variant?: OverridableStringUnion<
|
||||
'text' | 'outlined' | 'contained',
|
||||
ButtonPropsVariantOverrides
|
||||
>;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
@ -29,6 +35,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
environmentId,
|
||||
projectId,
|
||||
endIcon,
|
||||
variant,
|
||||
...rest
|
||||
}) => {
|
||||
const smallScreen = useMediaQuery(`(max-width:${maxWidth})`);
|
||||
@ -55,7 +62,7 @@ const ResponsiveButton: React.FC<IResponsiveButtonProps> = ({
|
||||
permission={permission}
|
||||
projectId={projectId}
|
||||
color='primary'
|
||||
variant='contained'
|
||||
variant={variant}
|
||||
disabled={disabled}
|
||||
environmentId={environmentId}
|
||||
endIcon={endIcon}
|
||||
|
@ -114,6 +114,11 @@ export const ProjectFeatureToggles = ({
|
||||
|
||||
const isPlaceholder = Boolean(initialLoad || (loading && total));
|
||||
|
||||
const onboardingStarted =
|
||||
onboardingUIEnabled && project.onboardingStatus.status !== 'onboarded';
|
||||
const hasMultipleFeaturesOrNotOnboarding =
|
||||
(total !== undefined && total > 1) || !onboardingStarted;
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
@ -399,10 +404,7 @@ export const ProjectFeatureToggles = ({
|
||||
return (
|
||||
<Container>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
onboardingUIEnabled &&
|
||||
project.onboardingStatus.status !== 'onboarded'
|
||||
}
|
||||
condition={onboardingStarted}
|
||||
show={
|
||||
<ProjectOnboarding
|
||||
projectId={projectId}
|
||||
@ -410,111 +412,126 @@ export const ProjectFeatureToggles = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageContent
|
||||
disableLoading
|
||||
disablePadding
|
||||
header={
|
||||
<ProjectFeatureTogglesHeader
|
||||
isLoading={initialLoad}
|
||||
totalItems={total}
|
||||
searchQuery={tableState.query || ''}
|
||||
onChangeSearchQuery={(query) => {
|
||||
setTableState({ query });
|
||||
}}
|
||||
dataToExport={data}
|
||||
environmentsToExport={environments}
|
||||
actions={
|
||||
<ColumnsMenu
|
||||
columns={[
|
||||
{
|
||||
header: 'Name',
|
||||
id: 'name',
|
||||
isVisible: columnVisibility.name,
|
||||
isStatic: true,
|
||||
},
|
||||
{
|
||||
header: 'Created',
|
||||
id: 'createdAt',
|
||||
isVisible: columnVisibility.createdAt,
|
||||
},
|
||||
{
|
||||
header: 'By',
|
||||
id: 'createdBy',
|
||||
isVisible: columnVisibility.createdBy,
|
||||
},
|
||||
{
|
||||
header: 'Last seen',
|
||||
id: 'lastSeenAt',
|
||||
isVisible: columnVisibility.lastSeenAt,
|
||||
},
|
||||
{
|
||||
header: 'Lifecycle',
|
||||
id: 'lifecycle',
|
||||
isVisible: columnVisibility.lifecycle,
|
||||
},
|
||||
{
|
||||
id: 'divider',
|
||||
},
|
||||
...environments.map((environment) => ({
|
||||
header: environment,
|
||||
id: formatEnvironmentColumnId(
|
||||
environment,
|
||||
),
|
||||
isVisible:
|
||||
columnVisibility[
|
||||
formatEnvironmentColumnId(
|
||||
environment,
|
||||
)
|
||||
],
|
||||
})),
|
||||
]}
|
||||
onToggle={onToggleColumnVisibility}
|
||||
<ConditionallyRender
|
||||
condition={hasMultipleFeaturesOrNotOnboarding}
|
||||
show={
|
||||
<PageContent
|
||||
disableLoading
|
||||
disablePadding
|
||||
header={
|
||||
<ProjectFeatureTogglesHeader
|
||||
isLoading={initialLoad}
|
||||
totalItems={total}
|
||||
searchQuery={tableState.query || ''}
|
||||
onChangeSearchQuery={(query) => {
|
||||
setTableState({ query });
|
||||
}}
|
||||
dataToExport={data}
|
||||
environmentsToExport={environments}
|
||||
actions={
|
||||
<ColumnsMenu
|
||||
columns={[
|
||||
{
|
||||
header: 'Name',
|
||||
id: 'name',
|
||||
isVisible:
|
||||
columnVisibility.name,
|
||||
isStatic: true,
|
||||
},
|
||||
{
|
||||
header: 'Created',
|
||||
id: 'createdAt',
|
||||
isVisible:
|
||||
columnVisibility.createdAt,
|
||||
},
|
||||
{
|
||||
header: 'By',
|
||||
id: 'createdBy',
|
||||
isVisible:
|
||||
columnVisibility.createdBy,
|
||||
},
|
||||
{
|
||||
header: 'Last seen',
|
||||
id: 'lastSeenAt',
|
||||
isVisible:
|
||||
columnVisibility.lastSeenAt,
|
||||
},
|
||||
{
|
||||
header: 'Lifecycle',
|
||||
id: 'lifecycle',
|
||||
isVisible:
|
||||
columnVisibility.lifecycle,
|
||||
},
|
||||
{
|
||||
id: 'divider',
|
||||
},
|
||||
...environments.map(
|
||||
(environment) => ({
|
||||
header: environment,
|
||||
id: formatEnvironmentColumnId(
|
||||
environment,
|
||||
),
|
||||
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' }}
|
||||
>
|
||||
<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}
|
||||
|
||||
{'feature' in project.onboardingStatus ? (
|
||||
<ConnectSdkDialog
|
||||
open={connectSdkOpen}
|
||||
onClose={() => {
|
||||
setConnectSdkOpen(false);
|
||||
}}
|
||||
project={projectId}
|
||||
environments={environments}
|
||||
feature={project.onboardingStatus.feature}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</PageContent>
|
||||
/>
|
||||
{'feature' in project.onboardingStatus ? (
|
||||
<ConnectSdkDialog
|
||||
open={connectSdkOpen}
|
||||
onClose={() => {
|
||||
setConnectSdkOpen(false);
|
||||
}}
|
||||
project={projectId}
|
||||
environments={environments}
|
||||
feature={project.onboardingStatus.feature}
|
||||
/>
|
||||
) : null}
|
||||
<BatchSelectionActionsBar count={selectedData.length}>
|
||||
<ProjectFeaturesBatchActions
|
||||
selectedIds={Object.keys(rowSelection)}
|
||||
|
@ -26,6 +26,8 @@ import { useFeedback } from 'component/feedbackNew/useFeedback';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { CreateFeatureDialog } from './CreateFeatureDialog';
|
||||
import IosShare from '@mui/icons-material/IosShare';
|
||||
import type { OverridableStringUnion } from '@mui/types';
|
||||
import type { ButtonPropsVariantOverrides } from '@mui/material/Button/Button';
|
||||
|
||||
interface IProjectFeatureTogglesHeaderProps {
|
||||
isLoading?: boolean;
|
||||
@ -37,11 +39,22 @@ interface IProjectFeatureTogglesHeaderProps {
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
interface IFlagCreationButtonProps {
|
||||
text?: string;
|
||||
variant?: OverridableStringUnion<
|
||||
'text' | 'outlined' | 'contained',
|
||||
ButtonPropsVariantOverrides
|
||||
>;
|
||||
}
|
||||
|
||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||
whiteSpace: 'nowrap',
|
||||
}));
|
||||
|
||||
export const FlagCreationButton: FC = () => {
|
||||
export const FlagCreationButton = ({
|
||||
variant,
|
||||
text = 'New feature flag',
|
||||
}: IFlagCreationButtonProps) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const showCreateDialog = Boolean(searchParams.get('create'));
|
||||
@ -56,10 +69,11 @@ export const FlagCreationButton: FC = () => {
|
||||
Icon={Add}
|
||||
projectId={projectId}
|
||||
disabled={loading}
|
||||
variant={variant}
|
||||
permission={CREATE_FEATURE}
|
||||
data-testid='NAVIGATE_TO_CREATE_FEATURE'
|
||||
>
|
||||
New feature flag
|
||||
{text}
|
||||
</StyledResponsiveButton>
|
||||
<CreateFeatureDialog
|
||||
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 { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import { FlagCreationButton } from '../ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader';
|
||||
@ -76,13 +76,24 @@ const TypeCircleContainer = styled(MainCircleContainer)(({ theme }) => ({
|
||||
borderRadius: '20%',
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link)({
|
||||
const FlagLink = styled(Link)({
|
||||
fontWeight: 'bold',
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
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 = ({
|
||||
projectId,
|
||||
setConnectSdkOpen,
|
||||
@ -119,7 +130,9 @@ export const WelcomeToProject = ({
|
||||
Connect an SDK
|
||||
</TitleContainer>
|
||||
<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>
|
||||
<ResponsiveButton
|
||||
onClick={() => {
|
||||
@ -150,13 +163,12 @@ const CreateFlag = () => {
|
||||
<div>The project currently holds no feature toggles.</div>
|
||||
<div>Create a feature flag to get started.</div>
|
||||
</Typography>
|
||||
<FlagCreationButton />
|
||||
<FlagCreationButton text='Create flag' />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => {
|
||||
const theme = useTheme();
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const { featureTypes } = useFeatureTypes();
|
||||
const IconComponent = getFeatureTypeIcons(feature.type);
|
||||
@ -166,7 +178,7 @@ const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => {
|
||||
const typeTitle = `${typeName || feature.type} flag`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingFlagContainer>
|
||||
<TitleContainer>
|
||||
<MainCircleContainer>✓</MainCircleContainer>
|
||||
Create a feature flag
|
||||
@ -177,16 +189,21 @@ const ExistingFlag = ({ featureId, projectId }: IExistingFlagsProps) => {
|
||||
<IconComponent />
|
||||
</TypeCircleContainer>
|
||||
</HtmlTooltip>
|
||||
<StyledLink
|
||||
<FlagLink
|
||||
to={`/projects/${projectId}/features/${feature.name}`}
|
||||
>
|
||||
{feature.name}
|
||||
</StyledLink>
|
||||
</FlagLink>
|
||||
<Link to={`/projects/${projectId}/features/${feature.name}`}>
|
||||
view flag
|
||||
</Link>
|
||||
</TitleContainer>
|
||||
<Typography>
|
||||
Your project is not yet connected to any SDK. In order to start
|
||||
using your feature flag connect an SDK to the project.
|
||||
</Typography>
|
||||
</>
|
||||
<FlagCreationContainer>
|
||||
<FlagCreationButton
|
||||
variant='outlined'
|
||||
text='Create a new flag'
|
||||
/>
|
||||
</FlagCreationContainer>
|
||||
</ExistingFlagContainer>
|
||||
);
|
||||
};
|
||||
|
@ -86,6 +86,7 @@ export class OnboardingReadModel implements IOnboardingReadModel {
|
||||
const feature = await this.db('features')
|
||||
.select('name')
|
||||
.where('project', projectId)
|
||||
.where('archived_at', null)
|
||||
.first();
|
||||
|
||||
if (!feature) {
|
||||
|
Loading…
Reference in New Issue
Block a user