Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { FC, useState } from 'react'
import { useHistory } from 'react-router-dom'
import Button from 'components/base/forms/Button'
import Link from 'components/base/link'
import Icon from 'components/icons/Icon'
import OnboardingHeader from 'components/pages/onboarding/OnboardingHeader'
import ThemeToggle from 'components/ThemeToggle'
Expand Down Expand Up @@ -30,6 +31,7 @@ const OnboardingFlow: FC = () => {
environment,
environmentKey,
featureName: bootstrappedFeatureName,
hasDemoFlag,
organisationId,
organisationName,
projectId,
Expand Down Expand Up @@ -197,6 +199,27 @@ const OnboardingFlow: FC = () => {
)
}

// ensureFlag seeded nothing, so there is no flag to tour with.
if (!hasDemoFlag) {
return (
<div className='onboarding-flow mx-auto text-center'>
<h2 className='mb-2'>You’re already set up</h2>
<p className='text-muted mb-3'>
{projectDisplayName} already has flags, so we haven’t added a demo
one.
</p>
<div className='d-flex justify-content-center align-items-center gap-3'>
<Button onClick={skipToApp}>Go to your projects</Button>
<Link
to={`/project/${projectId}/environment/${environmentKey}/features`}
>
View flags in {projectDisplayName}
</Link>
</div>
</div>
)
}

return (
<div className='onboarding-flow mx-auto d-flex flex-column gap-4'>
<div className='d-flex justify-content-end'>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { ProjectFlag, Tag } from 'common/types/responses'
import {
DEMO_FLAG_NAME,
findDemoFlag,
shouldSeedDemoFlag,
} from 'components/pages/onboarding/bootstrap/demoFlag'

const flag = (name: string, tags: number[] = []): ProjectFlag =>
({ id: name.length, name, tags } as ProjectFlag)

const onboardingTag = { id: 7, label: 'Onboarding' } as Tag

describe('shouldSeedDemoFlag', () => {
it('seeds into an empty project', () => {
expect(shouldSeedDemoFlag([])).toBe(true)
})

it('seeds nothing once the project has flags of its own', () => {
expect(shouldSeedDemoFlag([flag('checkout_v2')])).toBe(false)
})
})

describe('findDemoFlag', () => {
it('finds a previous run by its tag, whatever it was renamed to', () => {
const renamed = flag('my_own_name', [onboardingTag.id])
expect(findDemoFlag([flag('checkout_v2'), renamed], onboardingTag)).toBe(
renamed,
)
})

it('prefers the tag over the name when both are present', () => {
const tagged = flag('renamed_demo', [onboardingTag.id])
const named = flag(DEMO_FLAG_NAME)
expect(findDemoFlag([named, tagged], onboardingTag)).toBe(tagged)
})

it('falls back to the name when the tag is missing', () => {
const demo = flag(DEMO_FLAG_NAME)
expect(findDemoFlag([flag('checkout_v2'), demo], undefined)).toBe(demo)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

it('finds nothing in a project that never ran the tour', () => {
expect(findDemoFlag([flag('checkout_v2')], onboardingTag)).toBeUndefined()
})

it('finds nothing in an empty project', () => {
expect(findDemoFlag([], onboardingTag)).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,23 @@ import {
ProjectSummary,
Tag,
} from 'common/types/responses'
import { SmartDefaults } from './useSmartDefaults'
import {
DEMO_FLAG_NAME,
ONBOARDING_TAG,
findDemoFlag,
shouldSeedDemoFlag,
} from './demoFlag'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import { SmartDefaults } from 'components/pages/onboarding/hooks/useSmartDefaults'
import { createOrganisationViaAccountStore } from './createOrganisationViaAccountStore'
import API from 'project/api'
import Constants from 'common/constants'

type Store = ReturnType<typeof getStore>

const FLAG_NAME = 'show_demo_button'
const DEFAULT_ORG_NAME = 'My organisation'
const DEFAULT_PROJECT_NAME = 'My first project'
const DEV_ENVIRONMENT_NAME = 'Development'
const PROD_ENVIRONMENT_NAME = 'Production'
const ONBOARDING_TAG = {
color: '#3cb371',
description: 'Created during onboarding',
label: 'Onboarding',
}

type ExistingOrg = { id: number; name: string }

export type BootstrapInput = {
Expand All @@ -47,6 +46,8 @@ export type OnboardingBootstrap = {
project: ProjectSummary
environment: Environment
featureName: string
// False when the project already had flags, so nothing was seeded.
hasDemoFlag: boolean
}

async function ensureOrganisation(
Expand Down Expand Up @@ -154,30 +155,28 @@ async function ensureFlag(
}),
)
.unwrap()
const results = flags?.results ?? []
const onboardingTag = await findOnboardingTag(store, project.id)
const existing =
(onboardingTag &&
flags?.results?.find((f) => f.tags?.includes(onboardingTag.id))) ||
flags?.results?.find((f) => f.name === FLAG_NAME)
const existing = findDemoFlag(results, onboardingTag)
if (existing) {
return existing
}
const isFirstFeature = !flags?.results?.length
if (!shouldSeedDemoFlag(results)) {
return undefined
}
const created = await store
.dispatch(
projectFlagService.endpoints.createProjectFlag.initiate({
body: {
name: FLAG_NAME,
name: DEMO_FLAG_NAME,
project: project.id,
type: 'STANDARD',
} as Req['createProjectFlag']['body'],
project_id: project.id,
}),
)
.unwrap()
if (isFirstFeature) {
API.trackEvent(Constants.events.CREATE_FIRST_FEATURE)
}
API.trackEvent(Constants.events.CREATE_FIRST_FEATURE)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return created
}

Expand Down Expand Up @@ -227,7 +226,8 @@ export async function bootstrapOnboarding(
AppActions.refreshOrganisation()
return {
environment,
featureName: flag?.name ?? FLAG_NAME,
featureName: flag?.name ?? DEMO_FLAG_NAME,
hasDemoFlag: !!flag,
organisationId: organisation.id,
organisationName: organisation.name,
project,
Expand Down
23 changes: 23 additions & 0 deletions frontend/web/components/pages/onboarding/bootstrap/demoFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ProjectFlag, Tag } from 'common/types/responses'

export const DEMO_FLAG_NAME = 'show_demo_button'

export const ONBOARDING_TAG = {
color: '#3cb371',
description: 'Created during onboarding',
label: 'Onboarding',
}

// A previous run's flag. Tag first: renaming is a delete and recreate, so the
// name alone is not reliable.
export const findDemoFlag = (
flags: ProjectFlag[],
onboardingTag?: Tag,
): ProjectFlag | undefined =>
(onboardingTag && flags.find((f) => f.tags?.includes(onboardingTag.id))) ||
flags.find((f) => f.name === DEMO_FLAG_NAME)

// Only seed into an empty project: features are project-level, so an unwanted
// flag shows up in every environment, production included.
export const shouldSeedDemoFlag = (flags: ProjectFlag[]): boolean =>
!flags.length
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import useSelectedOrganisation from 'common/hooks/useSelectedOrganisation'
import { useGetProfileQuery } from 'common/services/useProfile'
import { Environment } from 'common/types/responses'
import { useSmartDefaults } from './useSmartDefaults'
import { bootstrapOnboarding } from './bootstrapOnboarding'
import { bootstrapOnboarding } from 'components/pages/onboarding/bootstrap/bootstrapOnboarding'

export type OnboardingResourcesStatus = 'creating' | 'ready' | 'error'

Expand All @@ -15,6 +15,7 @@ export type OnboardingResources = {
organisationName: string
projectName: string
featureName: string
hasDemoFlag: boolean
caseSensitive: boolean
environment: Environment | null
environmentKey: string
Expand Down Expand Up @@ -46,6 +47,7 @@ export const useEnsureOnboardingResources = (): OnboardingResources => {
const [organisationName, setOrganisationName] = useState('')
const [projectName, setProjectName] = useState('')
const [featureName, setFeatureName] = useState('')
const [hasDemoFlag, setHasDemoFlag] = useState(true)
// Whether the project enforces lower-case feature names; drives the same name
// normalisation the create-feature modal applies (see the header).
const [caseSensitive, setCaseSensitive] = useState(false)
Expand Down Expand Up @@ -77,6 +79,7 @@ export const useEnsureOnboardingResources = (): OnboardingResources => {
setEnvironment(res.environment)
setEnvironmentKey(res.environment.api_key)
setFeatureName(res.featureName)
setHasDemoFlag(res.hasDemoFlag)
setStatus('ready')
})
.catch((e) => {
Expand All @@ -91,6 +94,7 @@ export const useEnsureOnboardingResources = (): OnboardingResources => {
environmentKey,
error,
featureName,
hasDemoFlag,
organisationId,
organisationName,
projectId,
Expand Down
Loading