Skip to content
Open
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
12 changes: 12 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,18 @@ export type Req = {
hypothesis: string
feature: number
metrics: { metric: number; expected_direction: ExpectedDirection }[]
experiment_rollout: {
enabled: boolean
rollout_percentage: number
feature_state_value: {
type: 'integer' | 'string' | 'boolean'
value: string
}
multivariate_feature_state_values: {
multivariate_feature_option: number
percentage_allocation: number
}[]
}
}
}
experimentAction: { environmentId: string; experimentId: number }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
border: 1px solid var(--color-border-default);
border-radius: var(--radius-lg);

&--white {
background: var(--color-surface-default);
}

&__heading {
display: flex;
flex-direction: column;
gap: 6px;
}

&__header {
Expand All @@ -35,7 +40,7 @@

&__title {
font-size: var(--font-body-size, 0.875rem);
font-weight: var(--font-weight-regular, 400);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
margin: 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type ContentCardProps = {
action?: ReactNode
className?: string
compact?: boolean
white?: boolean
children: ReactNode
}

Expand All @@ -18,12 +19,14 @@ const ContentCard: FC<ContentCardProps> = ({
compact,
description,
title,
white,
}) => {
return (
<div
className={cn(
'content-card',
compact && 'content-card--compact',
white && 'content-card--white',
className,
)}
>
Expand Down
81 changes: 74 additions & 7 deletions frontend/web/components/experiments/CreateExperimentWizard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { FC, useCallback, useMemo, useState } from 'react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { ExpectedDirection, Metric, ProjectFlag } from 'common/types/responses'
import { useCreateExperimentMutation } from 'common/services/useExperiment'
import { useGetFeatureStatesQuery } from 'common/services/useFeatureState'
import { useProjectEnvironments } from 'common/hooks/useProjectEnvironments'
import { METRIC_DIRECTION_TO_EXPECTED_DIRECTION } from './constants'
import WizardStepper from './WizardStepper'
import WizardNavButtons from './WizardNavButtons'
import LivePreviewPanel from './LivePreviewPanel'
import SetupStep from './steps/SetupStep'
import AudienceStep from './steps/AudienceStep'
import RolloutStep from './steps/RolloutStep'
import {
VariationSplitEntry,
getControlPercentage,
getVariationSplitDefaults,
toRolloutFeatureValue,
} from './rollout'
import MeasurementStep from './steps/MeasurementStep'
import ReviewStep from './steps/ReviewStep'

Expand Down Expand Up @@ -34,8 +42,39 @@ const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
const [selectedMetric, setSelectedMetric] = useState<Metric | null>(null)
const [expectedDirection, setExpectedDirection] =
useState<ExpectedDirection | null>(null)
const [rolloutPercentage, setRolloutPercentage] = useState(100)
const [variationSplit, setVariationSplit] = useState<VariationSplitEntry[]>(
[],
)
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set())

const { getEnvironmentIdFromKey } = useProjectEnvironments(projectId)
const numericEnvId = getEnvironmentIdFromKey(environmentId)

const { data: featureStatesData } = useGetFeatureStatesQuery(
{ environment: numericEnvId, feature: selectedFeature?.id },
{ skip: !selectedFeature || !numericEnvId },
)

const environmentFeatureState = useMemo(
() =>
featureStatesData?.results?.find(
(state) => !state.feature_segment && !state.identity,
),
[featureStatesData],
)

useEffect(() => {
setVariationSplit(
selectedFeature
? getVariationSplitDefaults(
selectedFeature.multivariate_options,
environmentFeatureState?.multivariate_feature_state_values,
)
: [],
)
}, [selectedFeature, environmentFeatureState])

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

Expand All @@ -50,9 +89,14 @@ const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
const isMeasurementValid =
selectedMetric !== null && expectedDirection !== null

const controlPercentage = getControlPercentage(variationSplit)
const isRolloutValid =
rolloutPercentage > 0 && controlPercentage >= 0 && controlPercentage <= 100

const stepValidity: Record<number, boolean> = {
0: isStep1Valid,
3: isStep1Valid && isMeasurementValid,
1: isRolloutValid,
3: isStep1Valid && isRolloutValid && isMeasurementValid,
[MEASUREMENT_STEP]: isMeasurementValid,
}
const canContinue = stepValidity[currentStep] ?? true
Expand Down Expand Up @@ -89,8 +133,16 @@ const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
const doCreate = useCallback(async () => {
if (!selectedFeature || !selectedMetric || !expectedDirection) return
try {
const controlValue =
selectedFeature.environment_feature_state?.feature_state_value ?? ''
await createExperiment({
body: {
experiment_rollout: {
enabled: false,
feature_state_value: toRolloutFeatureValue(controlValue),
multivariate_feature_state_values: variationSplit,
rollout_percentage: rolloutPercentage,
},
feature: selectedFeature.id,
hypothesis: hypothesis.trim(),
metrics: [
Expand All @@ -115,8 +167,10 @@ const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
hypothesis,
name,
onCreated,
rolloutPercentage,
selectedFeature,
selectedMetric,
variationSplit,
])

const handleLaunch = useCallback(() => {
Expand All @@ -126,16 +180,18 @@ const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
<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.
<strong>
{rolloutPercentage}% of eligible identities 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, isMeasurementValid, doCreate])
}, [selectedFeature, isMeasurementValid, rolloutPercentage, doCreate])

const renderStep = () => {
switch (currentStep) {
Expand All @@ -153,7 +209,15 @@ const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
/>
)
case 1:
return <AudienceStep />
return (
<RolloutStep
selectedFeature={selectedFeature}
rolloutPercentage={rolloutPercentage}
variationSplit={variationSplit}
onRolloutChange={setRolloutPercentage}
onSplitChange={setVariationSplit}
/>
)
case 2:
return (
<MeasurementStep
Expand All @@ -172,8 +236,11 @@ const CreateExperimentWizard: FC<CreateExperimentWizardProps> = ({
selectedFeature={selectedFeature}
selectedMetric={selectedMetric}
expectedDirection={expectedDirection}
rolloutPercentage={rolloutPercentage}
variationSplit={variationSplit}
onEditSetup={() => setCurrentStep(0)}
onEditMeasurement={() => setCurrentStep(MEASUREMENT_STEP)}
onEditRollout={() => setCurrentStep(1)}
/>
)
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.distribution-bar {
display: flex;
height: 10px;
width: 100%;
border-radius: var(--radius-full);
overflow: hidden;
background: var(--color-surface-emphasis);

&__segment {
height: 100%;
transition: width var(--duration-fast) var(--easing-standard);
}

&__segment--hatched {
background: repeating-linear-gradient(
45deg,
var(--color-surface-emphasis),
var(--color-surface-emphasis) 5px,
var(--color-surface-default) 5px,
var(--color-surface-default) 10px
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FC } from 'react'
import cn from 'classnames'
import './DistributionBar.scss'

export type DistributionBarSegment = {
key: string
weight: number
colour?: string
hatched?: boolean
}

type DistributionBarProps = {
segments: DistributionBarSegment[]
className?: string
}

const DistributionBar: FC<DistributionBarProps> = ({ className, segments }) => (
<div className={cn('distribution-bar', className)}>
{segments.map((segment) =>
segment.weight > 0 ? (
<div
key={segment.key}
className={cn('distribution-bar__segment', {
'distribution-bar__segment--hatched': segment.hatched,
})}
style={{
background: segment.hatched ? undefined : segment.colour,
width: `${segment.weight}%`,
}}
/>
) : null,
)}
</div>
)

export default DistributionBar
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './DistributionBar'
export type { DistributionBarSegment } from './DistributionBar'
Loading
Loading