diff --git a/frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx b/frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx new file mode 100644 index 000000000000..bbbf3e0802fa --- /dev/null +++ b/frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from 'storybook' + +import OnboardingNextSteps from 'components/pages/onboarding/OnboardingNextSteps' + +const meta: Meta = { + args: { + locked: false, + onSelect: () => {}, + }, + component: OnboardingNextSteps, + parameters: { + docs: { + description: { + component: + 'The "Choose your next quest" section at the bottom of the onboarding flow: the three ways the demo flag can level up (gradual rollout, experiment, remote config), each linking to its real config. Locked and dimmed until the app connects.', + }, + }, + layout: 'padded', + }, + title: 'Pages/Onboarding/OnboardingNextSteps', +} +export default meta + +type Story = StoryObj + +export const Connected: Story = {} + +export const Locked: Story = { + args: { locked: true }, +} diff --git a/frontend/e2e/tests/onboarding-tests.pw.ts b/frontend/e2e/tests/onboarding-tests.pw.ts index a529760f6399..762da7b82585 100644 --- a/frontend/e2e/tests/onboarding-tests.pw.ts +++ b/frontend/e2e/tests/onboarding-tests.pw.ts @@ -48,6 +48,11 @@ test.describe('Onboarding', () => { await expect(page.getByText('LISTENING')).toBeVisible(); await expect(page.getByText('Copy install command')).not.toContainText('✓'); + // Before the first evaluation, the next-quest cards are locked. + await expect( + page.getByText('Unlocks after your first evaluation'), + ).toBeVisible(); + log('Copy snippets, checklist ticks'); await page.getByRole('button', { name: 'Copy install command' }).click(); await expect(page.getByText('Copy install command')).toContainText('✓'); @@ -101,5 +106,27 @@ test.describe('Onboarding', () => { .getByText('Onboarding', { exact: true }), ).toBeVisible(); await visualSnapshot(page, 'onboarding-renamed', testInfo); + + // The next-quest cards unlock once connected, each deep-linking to the + // flag's real config (nothing faked). + log('Next-quest cards link to the real config'); + await expect( + page.getByRole('heading', { name: 'Choose your next quest' }), + ).toBeVisible(); + + await page.getByRole('button', { name: /Gradual rollout/ }).click(); + await expect(page).toHaveURL( + /\/features\?feature=\d+&tab=segment-overrides/, + ); + + await page.goto('/getting-started?connected'); + await flowReady(); + await page.getByRole('button', { name: /Remote config/ }).click(); + await expect(page).toHaveURL(/\/features\?feature=\d+&tab=value/); + + await page.goto('/getting-started?connected'); + await flowReady(); + await page.getByRole('button', { name: /Experiment/ }).click(); + await expect(page).toHaveURL(/\/experiments$/); }); }); diff --git a/frontend/web/components/base/SelectableCard/SelectableCard.scss b/frontend/web/components/base/SelectableCard/SelectableCard.scss index 0315b3f4a852..72eae4590544 100644 --- a/frontend/web/components/base/SelectableCard/SelectableCard.scss +++ b/frontend/web/components/base/SelectableCard/SelectableCard.scss @@ -36,6 +36,8 @@ display: flex; flex-direction: column; gap: 4px; + flex: 1; + min-width: 0; } &__icon { diff --git a/frontend/web/components/base/SelectableCard/SelectableCard.tsx b/frontend/web/components/base/SelectableCard/SelectableCard.tsx index 3a614f228702..a88d42577312 100644 --- a/frontend/web/components/base/SelectableCard/SelectableCard.tsx +++ b/frontend/web/components/base/SelectableCard/SelectableCard.tsx @@ -1,11 +1,14 @@ import React, { FC, ReactNode } from 'react' +import cn from 'classnames' import BareButton from 'components/base/forms/BareButton' import './SelectableCard.scss' type BadgeVariant = 'primary' | 'secondary' type SelectableCardProps = { - selected: boolean + // Selection state. Omit for a card that acts on click without a chosen state + // (e.g. a card that navigates). + selected?: boolean onClick: () => void icon?: ReactNode title: string @@ -13,10 +16,15 @@ type SelectableCardProps = { badge?: { label: string; variant: BadgeVariant } tags?: string[] disabled?: boolean + className?: string + // Extra content below the description (e.g. an illustrative preview). + children?: ReactNode } const SelectableCard: FC = ({ badge, + children, + className, description, disabled, icon, @@ -27,9 +35,14 @@ const SelectableCard: FC = ({ }) => { return ( @@ -46,6 +59,7 @@ const SelectableCard: FC = ({ ))} )} + {children} {badge && (
diff --git a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.scss b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.scss index a55b52bb8bce..dc24f8b8f51a 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.scss +++ b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.scss @@ -8,6 +8,12 @@ font-size: 16px; } + // Lock affordance beside the title while the toggle is waiting to connect. + &__hint { + font-size: 13px; + color: var(--color-text-tertiary); + } + &__table { width: 100%; max-width: 760px; diff --git a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx index 2b3fcf580760..58dc7fffa825 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx @@ -4,6 +4,7 @@ import { Tag as TTag } from 'common/types/responses' import FeatureName from 'components/feature-summary/FeatureName' import Tag from 'components/tags/Tag' import Switch from 'components/Switch' +import Icon from 'components/icons/Icon' import './OnboardingFlagsTable.scss' export type OnboardingFlagsTableStatus = 'waiting' | 'connected' @@ -41,12 +42,20 @@ const OnboardingFlagsTable: FC = ({ className='onboarding-flags d-flex flex-column align-items-center' aria-labelledby='onboarding-flags-title' > -

- Your flags -

+
+

+ Your flags +

+ {waiting && ( + + + Flip it once your app connects + + )} +
{ const [snippetCopied, setSnippetCopied] = useState(false) const { enabled: flagEnabled, + flagId, isToggling, ready: flagStateReady, tags: flagTags, @@ -117,6 +121,24 @@ const OnboardingFlow: FC = () => { } } + // Each next-step card deep-links to the flag's real config; nothing faked. + const goToNextStep = (step: OnboardingNextStep) => { + if (projectId === null) { + return + } + const base = `/project/${projectId}/environment/${environmentKey}` + if (step === 'experiment') { + history.push(`${base}/experiments`) + return + } + if (flagId === null) { + return + } + // Tab param is the slugified tab label (see TabMenu/Tabs urlParam). + const tab = step === 'rollout' ? 'segment-overrides' : 'value' + history.push(`${base}/features?feature=${flagId}&tab=${tab}`) + } + if (status === 'creating') { return (
@@ -178,6 +200,10 @@ const OnboardingFlow: FC = () => { togglingFlag={isToggling ? featureName : null} togglesReady={flagStateReady} /> +