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'; } from 'recharts';
// ============ Sub-view type ============ // ============ Sub-view type ============
type SubView = 'list' | 'detail' | 'create'; type SubView = 'list' | 'detail' | 'create' | 'edit';
// ============ Constants ============ // ============ Constants ============
const STATUS_OPTIONS = ['planned', 'active', 'completed', 'paused']; const STATUS_OPTIONS = ['planned', 'active', 'completed', 'paused'];
@@ -71,15 +71,21 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ salesData = [], onExp
}, []); }, []);
const navigateToCreate = useCallback(() => { const navigateToCreate = useCallback(() => {
setSelectedId(null);
setSubView('create'); setSubView('create');
}, []); }, []);
const navigateToEdit = useCallback((id: string) => {
setSelectedId(id);
setSubView('edit');
}, []);
const navigateToList = useCallback(() => { const navigateToList = useCallback(() => {
setSelectedId(null); setSelectedId(null);
setSubView('list'); setSubView('list');
}, []); }, []);
const handleCreated = useCallback((id: string) => { const handleSaved = useCallback((id: string) => {
setSelectedId(id); setSelectedId(id);
setSubView('detail'); setSubView('detail');
onExperimentsFetch?.(); onExperimentsFetch?.();
@@ -99,14 +105,23 @@ const ExperimentsView: React.FC<ExperimentsViewProps> = ({ salesData = [], onExp
experimentId={selectedId} experimentId={selectedId}
salesData={salesData} salesData={salesData}
onBack={navigateToList} onBack={navigateToList}
onEdit={() => navigateToEdit(selectedId)}
onExperimentsFetch={onExperimentsFetch} onExperimentsFetch={onExperimentsFetch}
/> />
)} )}
{subView === 'create' && ( {subView === 'create' && (
<ExperimentCreateView <ExperimentFormView
salesData={salesData} salesData={salesData}
onBack={navigateToList} onBack={navigateToList}
onCreated={handleCreated} onSaved={handleSaved}
/>
)}
{subView === 'edit' && selectedId && (
<ExperimentEditWrapper
experimentId={selectedId}
salesData={salesData}
onBack={() => navigateToDetail(selectedId)}
onSaved={handleSaved}
/> />
)} )}
</div> </div>
@@ -355,8 +370,9 @@ const ExperimentDetailView: React.FC<{
experimentId: string; experimentId: string;
salesData: CombinedKPIs[]; salesData: CombinedKPIs[];
onBack: () => void; onBack: () => void;
onEdit: () => void;
onExperimentsFetch?: () => void; onExperimentsFetch?: () => void;
}> = ({ experimentId, salesData, onBack, onExperimentsFetch }) => { }> = ({ experimentId, salesData, onBack, onEdit, onExperimentsFetch }) => {
const [experiment, setExperiment] = useState<Experiment | null>(null); const [experiment, setExperiment] = useState<Experiment | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [analyzing, setAnalyzing] = useState(false); const [analyzing, setAnalyzing] = useState(false);
@@ -505,6 +521,15 @@ const ExperimentDetailView: React.FC<{
)} )}
Run Analysis Run Analysis
</button> </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 <button
onClick={handleDelete} onClick={handleDelete}
className="p-2 text-slate-400 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors" 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[]; salesData: CombinedKPIs[];
initialData?: Experiment;
onBack: () => void; onBack: () => void;
onCreated: (id: string) => void; onSaved: (id: string) => void;
}> = ({ salesData, onBack, onCreated }) => { }> = ({ salesData, initialData, onBack, onSaved }) => {
const [name, setName] = useState(''); const [name, setName] = useState(initialData?.name || '');
const [description, setDescription] = useState(''); const [description, setDescription] = useState(initialData?.description || '');
const [type, setType] = useState<ExperimentType>('content'); const [type, setType] = useState<ExperimentType>(initialData?.type || 'content');
const [marketplace, setMarketplace] = useState('DE'); const [marketplace, setMarketplace] = useState(initialData?.marketplace || 'DE');
const [owner, setOwner] = useState(''); const [owner, setOwner] = useState(initialData?.owner || '');
const [hypothesis, setHypothesis] = useState(''); const [hypothesis, setHypothesis] = useState(initialData?.hypothesis || '');
const [primaryMetric, setPrimaryMetric] = useState<ExperimentMetric>('units'); const [primaryMetric, setPrimaryMetric] = useState<ExperimentMetric>(initialData?.primary_metric || 'units');
const [startDate, setStartDate] = useState(new Date().toISOString().split('T')[0]); const [startDate, setStartDate] = useState(initialData?.start_date?.split('T')[0] || new Date().toISOString().split('T')[0]);
const [endDate, setEndDate] = useState(''); const [endDate, setEndDate] = useState(initialData?.end_date?.split('T')[0] || '');
const [enableCustomBaseline, setEnableCustomBaseline] = useState(false); const [enableCustomBaseline, setEnableCustomBaseline] = useState(!!(initialData?.baseline_start_date && initialData?.baseline_end_date));
const [baselineStartDate, setBaselineStartDate] = useState(''); const [baselineStartDate, setBaselineStartDate] = useState(initialData?.baseline_start_date?.split('T')[0] || '');
const [baselineEndDate, setBaselineEndDate] = useState(''); const [baselineEndDate, setBaselineEndDate] = useState(initialData?.baseline_end_date?.split('T')[0] || '');
const [treatmentAsins, setTreatmentAsins] = useState(''); const [treatmentAsins, setTreatmentAsins] = useState(initialData?.asins?.join(', ') || '');
const [controlAsins, setControlAsins] = useState(''); const [controlAsins, setControlAsins] = useState(initialData?.control_asins?.join(', ') || '');
const [changes, setChanges] = useState<ExperimentChangeAnnotation[]>([{ field: '', before_value: '', after_value: '' }]); const [changes, setChanges] = useState<ExperimentChangeAnnotation[]>(
initialData?.changes?.length ? initialData.changes : [{ field: '', before_value: '', after_value: '' }]
);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -913,8 +941,13 @@ const ExperimentCreateView: React.FC<{
changes: validChanges, changes: validChanges,
owner: owner.trim() || undefined, owner: owner.trim() || undefined,
}; };
if (initialData) {
const updated = await updateExperiment(initialData.id, input);
onSaved(updated.id);
} else {
const created = await createExperiment(input); const created = await createExperiment(input);
onCreated(created.id); onSaved(created.id);
}
} catch (e: any) { } catch (e: any) {
setError(e.message); setError(e.message);
} finally { } finally {
@@ -931,7 +964,9 @@ const ExperimentCreateView: React.FC<{
Back to experiments Back to experiments
</button> </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"> <div className="max-w-3xl space-y-8">
{/* Basic Info */} {/* 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" 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" />} {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>
<button onClick={onBack} className="px-4 py-2.5 text-slate-400 hover:text-white text-sm transition-colors"> <button onClick={onBack} className="px-4 py-2.5 text-slate-400 hover:text-white text-sm transition-colors">
Cancel 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 // SHARED UI COMPONENTS
// ==================================================================== // ====================================================================