feat(experiments): add edit experiment functionality

This commit is contained in:
Christian Vidal Wolf
2026-02-25 09:26:58 +01:00
parent 4de9fb5b89
commit 27d4268aff
+79 -27
View File
@@ -39,7 +39,7 @@ import {
} from 'recharts';
// ============ Sub-view type ============
type SubView = 'list' | 'detail' | 'create';
type SubView = 'list' | 'detail' | 'create' | 'edit';
// ============ Constants ============
const STATUS_OPTIONS = ['planned', 'active', 'completed', 'paused'];
@@ -71,15 +71,21 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ salesData = [], onExp
}, []);
const navigateToCreate = useCallback(() => {
setSelectedId(null);
setSubView('create');
}, []);
const navigateToEdit = useCallback((id: string) => {
setSelectedId(id);
setSubView('edit');
}, []);
const navigateToList = useCallback(() => {
setSelectedId(null);
setSubView('list');
}, []);
const handleCreated = useCallback((id: string) => {
const handleSaved = useCallback((id: string) => {
setSelectedId(id);
setSubView('detail');
onExperimentsFetch?.();
@@ -99,14 +105,23 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ salesData = [], onExp
experimentId={selectedId}
salesData={salesData}
onBack={navigateToList}
onEdit={() => navigateToEdit(selectedId)}
onExperimentsFetch={onExperimentsFetch}
/>
)}
{subView === 'create' && (
<ExperimentCreateView
<ExperimentFormView
salesData={salesData}
onBack={navigateToList}
onCreated={handleCreated}
onSaved={handleSaved}
/>
)}
{subView === 'edit' && selectedId && (
<ExperimentEditWrapper
experimentId={selectedId}
salesData={salesData}
onBack={() => navigateToDetail(selectedId)}
onSaved={handleSaved}
/>
)}
</div>
@@ -355,8 +370,9 @@ const ExperimentDetailView: React.FC<{
experimentId: string;
salesData: CombinedKPIs[];
onBack: () => void;
onEdit: () => void;
onExperimentsFetch?: () => void;
}> = ({ experimentId, salesData, onBack, onExperimentsFetch }) => {
}> = ({ experimentId, salesData, onBack, onEdit, onExperimentsFetch }) => {
const [experiment, setExperiment] = useState<Experiment | null>(null);
const [loading, setLoading] = useState(true);
const [analyzing, setAnalyzing] = useState(false);
@@ -505,6 +521,15 @@ const ExperimentDetailView: React.FC<{
)}
Run Analysis
</button>
<button
onClick={onEdit}
className="p-2 text-slate-400 hover:text-indigo-400 hover:bg-indigo-500/10 rounded-lg transition-colors"
title="Edit experiment"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" />
</svg>
</button>
<button
onClick={handleDelete}
className="p-2 text-slate-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
@@ -841,29 +866,32 @@ const ExperimentDetailView: React.FC<{
};
// ====================================================================
// CREATE VIEW
// FORM VIEW
// ====================================================================
const ExperimentCreateView: React.FC<{
const ExperimentFormView: React.FC<{
salesData: CombinedKPIs[];
initialData?: Experiment;
onBack: () => void;
onCreated: (id: string) => void;
}> = ({ salesData, onBack, onCreated }) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [type, setType] = useState<ExperimentType>('content');
const [marketplace, setMarketplace] = useState('DE');
const [owner, setOwner] = useState('');
const [hypothesis, setHypothesis] = useState('');
const [primaryMetric, setPrimaryMetric] = useState<ExperimentMetric>('units');
const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]);
const [endDate, setEndDate] = useState('');
const [enableCustomBaseline, setEnableCustomBaseline] = useState(false);
const [baselineStartDate, setBaselineStartDate] = useState('');
const [baselineEndDate, setBaselineEndDate] = useState('');
const [treatmentAsins, setTreatmentAsins] = useState('');
const [controlAsins, setControlAsins] = useState('');
const [changes, setChanges] = useState<ExperimentChangeAnnotation[]>([{ field: '', before_value: '', after_value: '' }]);
onSaved: (id: string) => void;
}> = ({ salesData, initialData, onBack, onSaved }) => {
const [name, setName] = useState(initialData?.name || '');
const [description, setDescription] = useState(initialData?.description || '');
const [type, setType] = useState<ExperimentType>(initialData?.type || 'content');
const [marketplace, setMarketplace] = useState(initialData?.marketplace || 'DE');
const [owner, setOwner] = useState(initialData?.owner || '');
const [hypothesis, setHypothesis] = useState(initialData?.hypothesis || '');
const [primaryMetric, setPrimaryMetric] = useState<ExperimentMetric>(initialData?.primary_metric || 'units');
const [startDate, setStartDate] = useState(initialData?.start_date?.split('T')[0] || new Date().toISOString().split('T')[0]);
const [endDate, setEndDate] = useState(initialData?.end_date?.split('T')[0] || '');
const [enableCustomBaseline, setEnableCustomBaseline] = useState(!!(initialData?.baseline_start_date && initialData?.baseline_end_date));
const [baselineStartDate, setBaselineStartDate] = useState(initialData?.baseline_start_date?.split('T')[0] || '');
const [baselineEndDate, setBaselineEndDate] = useState(initialData?.baseline_end_date?.split('T')[0] || '');
const [treatmentAsins, setTreatmentAsins] = useState(initialData?.asins?.join(', ') || '');
const [controlAsins, setControlAsins] = useState(initialData?.control_asins?.join(', ') || '');
const [changes, setChanges] = useState<ExperimentChangeAnnotation[]>(
initialData?.changes?.length ? initialData.changes : [{ field: '', before_value: '', after_value: '' }]
);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
@@ -913,8 +941,13 @@ const ExperimentCreateView: React.FC<{
changes: validChanges,
owner: owner.trim() || undefined,
};
if (initialData) {
const updated = await updateExperiment(initialData.id, input);
onSaved(updated.id);
} else {
const created = await createExperiment(input);
onCreated(created.id);
onSaved(created.id);
}
} catch (e: any) {
setError(e.message);
} finally {
@@ -931,7 +964,9 @@ const ExperimentCreateView: React.FC<{
Back to experiments
</button>
<h1 className="text-2xl font-bold text-white tracking-tight mb-8">New Experiment</h1>
<h1 className="text-2xl font-bold text-white tracking-tight mb-8">
{initialData ? "Edit Experiment" : "New Experiment"}
</h1>
<div className="max-w-3xl space-y-8">
{/* Basic Info */}
@@ -1136,7 +1171,7 @@ const ExperimentCreateView: React.FC<{
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-lg font-medium text-sm transition-colors flex items-center gap-2"
>
{submitting && <div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" />}
Create Experiment
{initialData ? "Save Changes" : "Create Experiment"}
</button>
<button onClick={onBack} className="px-4 py-2.5 text-slate-400 hover:text-white text-sm transition-colors">
Cancel
@@ -1147,6 +1182,23 @@ const ExperimentCreateView: React.FC<{
);
};
const ExperimentEditWrapper: React.FC<{
experimentId: string;
salesData: CombinedKPIs[];
onBack: () => void;
onSaved: (id: string) => void;
}> = ({ experimentId, salesData, onBack, onSaved }) => {
const [experiment, setExperiment] = useState<Experiment | null>(null);
useEffect(() => {
getExperiment(experimentId).then(setExperiment);
}, [experimentId]);
if (!experiment) return <div className="flex items-center justify-center p-12 text-slate-400"><div className="animate-spin rounded-full h-8 w-8 border-2 border-indigo-500 border-t-transparent mr-4" /> Loading experiment data...</div>;
return <ExperimentFormView salesData={salesData} initialData={experiment} onBack={onBack} onSaved={onSaved} />;
};
// ====================================================================
// SHARED UI COMPONENTS
// ====================================================================