Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7eef7bf
feat(experimentation): add Experiment types to frontend
Zaimwa9 May 25, 2026
c835318
feat(experimentation): add RTK Query service for experiments
Zaimwa9 May 25, 2026
17360b0
feat(experimentation): add wizard utility components
Zaimwa9 May 25, 2026
c7d0bc2
feat(experimentation): add SetupStep with feature flag selector
Zaimwa9 May 25, 2026
07905c9
feat(experimentation): add AudienceStep, MeasurementStep, and ReviewStep
Zaimwa9 May 25, 2026
f71112d
feat(experimentation): add CreateExperimentWizard container
Zaimwa9 May 25, 2026
a60444f
feat(experimentation): rewrite ExperimentsPage with list and create w…
Zaimwa9 May 25, 2026
998df3c
refactor(experimentation): remove unused onCancel prop from wizard
Zaimwa9 May 25, 2026
a4519a9
feat(experimentation): add ContentCard component and replace Panel wi…
Zaimwa9 May 25, 2026
8ef2c9b
fix(experimentation): match POC layout with proper field styling and …
Zaimwa9 May 25, 2026
0173407
fix(experimentation): match POC stepper design with connecting lines …
Zaimwa9 May 25, 2026
6e02891
fix(experimentation): white background for hypothesis textarea
Zaimwa9 May 25, 2026
6b6fffb
fix(experimentation): resolve environment API key to numeric ID for f…
Zaimwa9 May 26, 2026
49ffcfc
feat(experimentation): add VariationTable component matching POC desi…
Zaimwa9 May 26, 2026
85102ae
fix(experimentation): rename to Create Experiment, add confirm modal,…
Zaimwa9 May 26, 2026
8aab7df
fix(experimentation): guard against null initial_value in VariationTable
Zaimwa9 May 26, 2026
10603f6
feat(experimentation): show warehouse setup prompt when no connection…
Zaimwa9 May 26, 2026
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
30 changes: 30 additions & 0 deletions frontend/common/services/useExperiment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'

export const experimentService = service
.enhanceEndpoints({ addTagTypes: ['Experiment'] })
.injectEndpoints({
endpoints: (builder) => ({
createExperiment: builder.mutation<
Res['experiment'],
Req['createExperiment']
>({
invalidatesTags: [{ id: 'LIST', type: 'Experiment' }],
query: ({ body, environmentId }) => ({
body,
method: 'POST',
url: `environments/${environmentId}/experiments/`,
}),
}),
getExperiments: builder.query<Res['experiments'], Req['getExperiments']>({
providesTags: [{ id: 'LIST', type: 'Experiment' }],
query: ({ environmentId }) => ({
url: `environments/${environmentId}/experiments/`,
}),
}),
}),
})

export const { useCreateExperimentMutation, useGetExperimentsQuery } =
experimentService
5 changes: 5 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -984,5 +984,10 @@ export type Req = {
name?: string
config?: Record<string, string>
}
getExperiments: { environmentId: string }
createExperiment: {
environmentId: string
body: { name: string; hypothesis: string; feature: number }
}
// END OF TYPES
}
16 changes: 16 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,20 @@ export type MultivariateOption = {

export type FeatureType = 'STANDARD' | 'MULTIVARIATE'

export type ExperimentStatus = 'created' | 'running' | 'paused' | 'completed'

export type Experiment = {
id: number
name: string
hypothesis: string
feature: number
status: ExperimentStatus
created_at: string
updated_at: string
started_at: string | null
ended_at: string | null
}

export enum TagStrategy {
INTERSECTION = 'INTERSECTION',
UNION = 'UNION',
Expand Down Expand Up @@ -1343,5 +1357,7 @@ export type Res = {
gitlabIssues: PagedResponse<GitLabIssue>
gitlabMergeRequests: PagedResponse<GitLabMergeRequest>
warehouseConnections: WarehouseConnection[]
experiments: Experiment[]
experiment: Experiment
// END OF TYPES
}
22 changes: 22 additions & 0 deletions frontend/web/components/base/grid/ContentCard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.content-card {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
background: var(--color-surface-subtle);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-lg);

&__header {
display: flex;
align-items: center;
justify-content: space-between;
}

&__title {
font-size: var(--font-body-size, 0.875rem);
font-weight: var(--font-weight-bold, 700);
color: var(--color-text-default);
margin: 0;
}
}
31 changes: 31 additions & 0 deletions frontend/web/components/base/grid/ContentCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FC, ReactNode } from 'react'
import cn from 'classnames'
import './ContentCard.scss'

type ContentCardProps = {
title?: string
action?: ReactNode
className?: string
children: ReactNode
}

const ContentCard: FC<ContentCardProps> = ({
action,
children,
className,
title,
}) => {
return (
<div className={cn('content-card', className)}>
{(title || action) && (
<div className='content-card__header'>
{title && <h3 className='content-card__title'>{title}</h3>}
{action}
</div>
)}
{children}
</div>
)
}

export default ContentCard
168 changes: 168 additions & 0 deletions frontend/web/components/experiments/CreateExperimentWizard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { FC, useCallback, useMemo, useState } from 'react'
import { ProjectFlag } from 'common/types/responses'
import { useCreateExperimentMutation } from 'common/services/useExperiment'
import WizardStepper from './WizardStepper'
import WizardNavButtons from './WizardNavButtons'
import LivePreviewPanel from './LivePreviewPanel'
import SetupStep from './steps/SetupStep'
import AudienceStep from './steps/AudienceStep'
import MeasurementStep from './steps/MeasurementStep'
import ReviewStep from './steps/ReviewStep'

const TOTAL_STEPS = 4

type CreateExperimentWizardProps = {
environmentId: string
projectId: number
onCreated: () => void
}

const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
environmentId,
onCreated,
projectId,
}) => {
const [currentStep, setCurrentStep] = useState(0)
const [name, setName] = useState('')
const [hypothesis, setHypothesis] = useState('')
const [selectedFeature, setSelectedFeature] = useState<ProjectFlag | null>(
null,
)
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set())

const [createExperiment, { isLoading: isSubmitting }] =
useCreateExperimentMutation()

const isStep1Valid = useMemo(
() =>
name.trim().length > 0 &&
hypothesis.trim().length > 0 &&
selectedFeature !== null,
[name, hypothesis, selectedFeature],
)

const canContinue = currentStep === 0 ? isStep1Valid : true

const handleContinue = useCallback(() => {
if (currentStep < TOTAL_STEPS - 1) {
setCompletedSteps((prev) => new Set(prev).add(currentStep))
setCurrentStep(currentStep + 1)
}
}, [currentStep])

const handleBack = useCallback(() => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1)
}
}, [currentStep])

const handleStepClick = useCallback(
(step: number) => {
if (completedSteps.has(step) || step < currentStep) {
setCurrentStep(step)
}
},
[completedSteps, currentStep],
)

const doCreate = useCallback(async () => {
if (!selectedFeature) return
try {
await createExperiment({
body: {
feature: selectedFeature.id,
hypothesis: hypothesis.trim(),
name: name.trim(),
},
environmentId,
}).unwrap()
toast('Experiment created successfully')
onCreated()
} catch {
toast('Failed to create experiment', 'danger')
}
}, [
createExperiment,
environmentId,
hypothesis,
name,
onCreated,
selectedFeature,
])

const handleLaunch = useCallback(() => {
if (!selectedFeature) return
openConfirm({
body: (
<span>
This will start serving variations of{' '}
<strong>{selectedFeature.name}</strong> to{' '}
<strong>100% of all users in the environment</strong>. You can pause
or stop the experiment at any time.
</span>
),
noText: 'Cancel',
onYes: doCreate,
title: 'Create experiment?',
yesText: 'Create',
})
}, [selectedFeature, doCreate])

const renderStep = () => {
switch (currentStep) {
case 0:
return (
<SetupStep
name={name}
hypothesis={hypothesis}
selectedFeature={selectedFeature}
projectId={projectId}
environmentId={environmentId}
onNameChange={setName}
onHypothesisChange={setHypothesis}
onFeatureSelect={setSelectedFeature}
/>
)
case 1:
return <AudienceStep />
case 2:
return <MeasurementStep />
case 3:
return (
<ReviewStep
name={name}
hypothesis={hypothesis}
selectedFeature={selectedFeature}
onEditSetup={() => setCurrentStep(0)}
/>
)
default:
return null
}
}

return (
<div className='d-flex gap-4'>
<WizardStepper
currentStep={currentStep}
completedSteps={completedSteps}
onStepClick={handleStepClick}
/>
<div className='flex-fill' style={{ minWidth: 0 }}>
{renderStep()}
<WizardNavButtons
currentStep={currentStep}
totalSteps={TOTAL_STEPS}
canContinue={canContinue}
isSubmitting={isSubmitting}
onBack={handleBack}
onContinue={handleContinue}
onLaunch={handleLaunch}
/>
</div>
<LivePreviewPanel />
</div>
)
}

export default CreateExperimentWizard
16 changes: 16 additions & 0 deletions frontend/web/components/experiments/LivePreviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FC } from 'react'
import ContentCard from 'components/base/grid/ContentCard'

const LivePreviewPanel: FC = () => {
return (
<div className='d-none d-xl-block' style={{ flexShrink: 0, width: 320 }}>
<ContentCard title='Live Preview'>
<p className='text-muted mb-0' style={{ fontSize: 13 }}>
Coming soon
</p>
</ContentCard>
</div>
)
}

export default LivePreviewPanel
Loading