-
Notifications
You must be signed in to change notification settings - Fork 536
feat(onboarding): connection terminal and flags table #7856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6d88a66
d192575
cdd1cb7
35b5022
eb2725e
202b3c5
82e4046
dbd2433
fe9913b
96e125e
d2474f9
63459ee
815647d
880188a
36c1a9e
79ae58f
28aef22
54ac8c7
547a18b
139ced3
13bfa10
2e3060d
5e78567
10f5c37
f9b0ac5
2fe5a1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { Meta, StoryObj } from 'storybook' | ||
|
|
||
| import { Tag } from 'common/types/responses' | ||
| import OnboardingFlagsTable, { | ||
| OnboardingFlagRow, | ||
| } from 'components/pages/onboarding/OnboardingFlagsTable' | ||
|
|
||
| const demoFlag: OnboardingFlagRow = { | ||
| description: 'Controls the demo button shown to your users', | ||
| enabled: true, | ||
| name: 'show_demo_button', | ||
| } | ||
|
|
||
| const meta: Meta<typeof OnboardingFlagsTable> = { | ||
| args: { | ||
| flags: [demoFlag], | ||
| onToggle: () => {}, | ||
| status: 'connected', | ||
| }, | ||
| component: OnboardingFlagsTable, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| component: | ||
| 'The "Your flags" card from the onboarding flow, reusing the product FeatureName / Tag / Switch. Prop-driven: the page owns the flag data and the persisted Dev toggle. `connected` lifts the card with the accent border and glow; `waiting` dims it until the first evaluation arrives.', | ||
| }, | ||
| }, | ||
| layout: 'padded', | ||
| }, | ||
| title: 'Pages/Onboarding/OnboardingFlagsTable', | ||
| } | ||
| export default meta | ||
|
|
||
| type Story = StoryObj<typeof OnboardingFlagsTable> | ||
|
|
||
| export const Connected: Story = {} | ||
|
|
||
| export const Waiting: Story = { | ||
| args: { status: 'waiting' }, | ||
| } | ||
|
|
||
| export const Off: Story = { | ||
| args: { flags: [{ ...demoFlag, enabled: false }] }, | ||
| } | ||
|
|
||
| export const WithTag: Story = { | ||
| args: { | ||
| flags: [ | ||
| { ...demoFlag, tags: [{ color: '#6837FC', label: 'demo' } as Tag] }, | ||
| ], | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import type { Meta, StoryObj } from 'storybook' | ||
|
|
||
| import OnboardingTerminal from 'components/pages/onboarding/OnboardingTerminal' | ||
|
|
||
| const meta: Meta<typeof OnboardingTerminal> = { | ||
| args: { | ||
| connected: false, | ||
| featureName: 'show_demo_button', | ||
| installCopied: false, | ||
| snippetCopied: false, | ||
| }, | ||
| component: OnboardingTerminal, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| component: | ||
| 'The onboarding verify console. The checklist ticks as the user acts (copy install, copy snippet), and the first evaluation flips the badge to LIVE and prints the connection receipt. Always dark, since a terminal reads the same in light and dark mode.', | ||
| }, | ||
| }, | ||
| layout: 'padded', | ||
| }, | ||
| title: 'Pages/Onboarding/OnboardingTerminal', | ||
| } | ||
| export default meta | ||
|
|
||
| type Story = StoryObj<typeof OnboardingTerminal> | ||
|
|
||
| export const Listening: Story = {} | ||
|
|
||
| export const InstallCopied: Story = { | ||
| args: { installCopied: true }, | ||
| } | ||
|
|
||
| export const SnippetsCopied: Story = { | ||
| args: { installCopied: true, snippetCopied: true }, | ||
| } | ||
|
|
||
| export const Connected: Story = { | ||
| args: { connected: true, installCopied: true, snippetCopied: true }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { test, expect } from '../test-setup'; | ||
| import { byId, createHelpers, getFlagsmith, log, visualSnapshot } from '../helpers'; | ||
| import { E2E_SIGN_UP_USER, PASSWORD } from '../config'; | ||
|
|
||
| // The single-page onboarding flow (onboarding_quickstart_flow), at | ||
| // /getting-started. Runs only when the flag is on (the legacy signup test runs | ||
| // when it's off). | ||
| test.describe('Onboarding', () => { | ||
| test('New user connects via the single-page onboarding flow @oss', async ({ | ||
| page, | ||
| }, testInfo) => { | ||
| const { addErrorLogging, click, setText, waitForElementVisible } = | ||
| createHelpers(page); | ||
| const flagsmith = await getFlagsmith(); | ||
|
|
||
| test.skip( | ||
| !flagsmith.hasFeature('onboarding_quickstart_flow'), | ||
| 'Onboarding flow is behind onboarding_quickstart_flow', | ||
| ); | ||
|
|
||
| await addErrorLogging(); | ||
|
|
||
| // The welcome heading means bootstrap settled (loader, then heading/error). | ||
| const flowReady = async () => | ||
| page | ||
| .getByRole('heading', { name: /Welcome/ }) | ||
| .waitFor({ state: 'visible', timeout: 30000 }); | ||
|
|
||
| log('Sign up'); | ||
| await page.goto('/'); | ||
| await click(byId('jsSignup')); | ||
| await waitForElementVisible(byId('firstName')); | ||
| await setText(byId('firstName'), 'Bullet'); | ||
| await setText(byId('lastName'), 'Train'); | ||
| await setText(byId('email'), E2E_SIGN_UP_USER); | ||
| await setText(byId('password'), PASSWORD); | ||
| await click(byId('signup-btn')); | ||
|
|
||
| // Don't navigate manually - a goto races the post-signup auth and bounces to | ||
| // /?redirect=. The app routes a getting-started user here itself, so just wait. | ||
| log('Land on the onboarding flow'); | ||
| await page.waitForURL((url) => url.pathname === '/getting-started', { | ||
| timeout: 30000, | ||
| }); | ||
| await flowReady(); | ||
| await visualSnapshot(page, 'onboarding-flow', testInfo); | ||
|
|
||
| await expect(page.getByText('LISTENING')).toBeVisible(); | ||
| await expect(page.getByText('Copy install command')).not.toContainText('✓'); | ||
|
|
||
| log('Copy snippets, checklist ticks'); | ||
| await page.getByRole('button', { name: 'Copy install command' }).click(); | ||
| await expect(page.getByText('Copy install command')).toContainText('✓'); | ||
| await page.getByRole('button', { name: 'Copy code snippet' }).click(); | ||
| await expect(page.getByText('Copy code snippet')).toContainText('✓'); | ||
|
|
||
| // No real first evaluation in a test, so force the connected state via | ||
| // ?connected (the #7767 stub); that unlocks the toggle and flips LIVE. | ||
| log('Force the connected state'); | ||
| await page.goto('/getting-started?connected'); | ||
| await flowReady(); | ||
| await expect(page.getByText('LIVE', { exact: true })).toBeVisible(); | ||
|
|
||
| // Two switches on the page (theme + flag), so scope to the flags region. | ||
| log('Toggle the flag'); | ||
| const flagsTable = page.getByRole('region', { name: 'Your flags' }); | ||
| const flagSwitch = flagsTable.getByRole('switch'); | ||
| await flagSwitch.waitFor({ state: 'visible' }); | ||
| const wasChecked = (await flagSwitch.getAttribute('class'))?.includes( | ||
| 'switch-checked', | ||
| ); | ||
| await flagSwitch.click(); | ||
| await expect(flagSwitch).toHaveClass( | ||
| wasChecked ? /switch-unchecked/ : /switch-checked/, | ||
| ); | ||
|
|
||
| // The Onboarding badge (attached in bootstrap) shows in the flags table. | ||
| // Exact match: the header crumb also contains the word "Onboarding". | ||
| await expect(flagsTable.getByText('Onboarding', { exact: true })).toBeVisible(); | ||
|
|
||
| // Rename the flag. Names are immutable, so this delete + recreates; the | ||
| // Onboarding tag must survive (the recreate carries the old flag's tags). | ||
| log('Rename the flag'); | ||
| const flagInput = page.getByLabel('Flag name'); | ||
| await flagInput.fill('renamed_demo_flag'); | ||
| await flagInput.press('Enter'); | ||
|
|
||
| // Wait for the rename to persist before reloading - it's a delete + recreate, | ||
| // and reloading too early aborts the requests. The toast fires once both land. | ||
| await expect(page.getByText('Flag name updated')).toBeVisible({ | ||
| timeout: 20000, | ||
| }); | ||
|
|
||
| // Reload to prove it persisted (bootstrap reuses the renamed flag). | ||
| await page.reload(); | ||
| await flowReady(); | ||
| await expect(page.getByLabel('Flag name')).toHaveValue('renamed_demo_flag'); | ||
| await expect( | ||
| page | ||
| .getByRole('region', { name: 'Your flags' }) | ||
| .getByText('Onboarding', { exact: true }), | ||
| ).toBeVisible(); | ||
| await visualSnapshot(page, 'onboarding-renamed', testInfo); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -136,8 +136,17 @@ const App = class extends Component { | |
| } | ||
|
|
||
| if (!AccountStore.getOrganisation() && !invite) { | ||
| // If user has no organisation redirect to /create | ||
| this.props.history.replace(`/create${query}`) | ||
| // New users with no organisation go through the single-page onboarding | ||
| // flow when it's enabled - it creates the organisation itself, so it | ||
| // replaces the legacy /create page. Everyone else still gets /create. | ||
| if ( | ||
| AccountStore.getUser()?.isGettingStarted && | ||
| Utils.getFlagsmithHasFeature('onboarding_quickstart_flow') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also gate L190 with the flag ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I've tested, this is the right place to gate it. A fresh signup has no org yet, so it hits this branch first and gets redirected to |
||
| ) { | ||
| this.props.history.replace('/getting-started') | ||
| } else { | ||
| this.props.history.replace(`/create${query}`) | ||
| } | ||
| return | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this has been added to the
UsertypeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, it wasn't. Added
isGettingStartedto theUsertype (optional, since it's set client-side at login rather than returned by the API). 139ced3.