chore(admin-web): clear repo-wide lint errors

This commit is contained in:
Saravana Achu Mac 2026-04-04 17:50:20 -07:00
parent 5e40cd1b6e
commit 631784e551
6 changed files with 256 additions and 211 deletions

View File

@ -1,9 +1,9 @@
import { test, expect } from '@playwright/test'; import { test, expect, type Page } from '@playwright/test';
const ADMIN_EMAIL = 'admin@example.com'; const ADMIN_EMAIL = 'admin@example.com';
const ADMIN_PASSWORD = 'Admin123!'; const ADMIN_PASSWORD = 'Admin123!';
async function loginAsAdmin(page: any) { async function loginAsAdmin(page: Page) {
await page.goto('/login'); await page.goto('/login');
await page.getByLabel('Email').fill(ADMIN_EMAIL); await page.getByLabel('Email').fill(ADMIN_EMAIL);
await page.getByLabel('Password').fill(ADMIN_PASSWORD); await page.getByLabel('Password').fill(ADMIN_PASSWORD);

View File

@ -1,9 +1,9 @@
import { test, expect } from '@playwright/test'; import { test, expect, type Page } from '@playwright/test';
const ADMIN_EMAIL = 'admin@example.com'; const ADMIN_EMAIL = 'admin@example.com';
const ADMIN_PASSWORD = 'Admin123!'; const ADMIN_PASSWORD = 'Admin123!';
async function loginAsAdmin(page: any) { async function loginAsAdmin(page: Page) {
await page.goto('/login'); await page.goto('/login');
await page.getByLabel('Email').fill(ADMIN_EMAIL); await page.getByLabel('Email').fill(ADMIN_EMAIL);
await page.getByLabel('Password').fill(ADMIN_PASSWORD); await page.getByLabel('Password').fill(ADMIN_PASSWORD);
@ -94,7 +94,11 @@ test.describe('Diagnostics - Debug Sessions', () => {
await expect(page.getByText('Session paused')).toBeVisible(); await expect(page.getByText('Session paused')).toBeVisible();
// Resume // Resume
await page.locator('tr:has-text("Paused")').first().getByRole('button', { name: 'Resume' }).click(); await page
.locator('tr:has-text("Paused")')
.first()
.getByRole('button', { name: 'Resume' })
.click();
await expect(page.getByText('Session resumed')).toBeVisible(); await expect(page.getByText('Session resumed')).toBeVisible();
} }
}); });
@ -165,7 +169,9 @@ test.describe('Diagnostics - Logs & Traces', () => {
await expect(page.locator('[data-testid="log-entry"]').first()).toBeVisible(); await expect(page.locator('[data-testid="log-entry"]').first()).toBeVisible();
// Check log level badges // Check log level badges
await expect(page.getByText('INFO').or(page.getByText('ERROR')).or(page.getByText('DEBUG'))).toBeVisible(); await expect(
page.getByText('INFO').or(page.getByText('ERROR')).or(page.getByText('DEBUG'))
).toBeVisible();
} }
}); });

View File

@ -1,9 +1,9 @@
import { test, expect } from '@playwright/test'; import { test, expect, type Page } from '@playwright/test';
const ADMIN_EMAIL = 'admin@example.com'; const ADMIN_EMAIL = 'admin@example.com';
const ADMIN_PASSWORD = 'Admin123!'; const ADMIN_PASSWORD = 'Admin123!';
async function loginAsAdmin(page: any) { async function loginAsAdmin(page: Page) {
await page.goto('/login'); await page.goto('/login');
await page.getByLabel('Email').fill(ADMIN_EMAIL); await page.getByLabel('Email').fill(ADMIN_EMAIL);
await page.getByLabel('Password').fill(ADMIN_PASSWORD); await page.getByLabel('Password').fill(ADMIN_PASSWORD);
@ -75,7 +75,10 @@ test.describe('Rich Media Broadcasts', () => {
// Go back to list and open preview // Go back to list and open preview
await page.click('text=Broadcasts'); await page.click('text=Broadcasts');
await page.locator('tr:has-text("Preview Test")').getByRole('button', { name: 'Preview' }).click(); await page
.locator('tr:has-text("Preview Test")')
.getByRole('button', { name: 'Preview' })
.click();
// Verify media in preview modal // Verify media in preview modal
await expect(page.locator('img[src*="preview.jpg"]')).toBeVisible(); await expect(page.locator('img[src*="preview.jpg"]')).toBeVisible();

View File

@ -23,11 +23,23 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import type { CreateExperimentInput, ExperimentSuggestion } from '@/lib/experiments-types'; import type {
AllocationStrategy,
CreateExperimentInput,
ExperimentSuggestion,
MetricType,
PrimaryMetric,
} from '@/lib/experiments-types';
const steps = [ const steps = [
{ id: 'hypothesis', title: 'Hypothesis', icon: Lightbulb }, { id: 'hypothesis', title: 'Hypothesis', icon: Lightbulb },
@ -49,7 +61,13 @@ export default function NewExperimentPage() {
description: '', description: '',
hypothesis: '', hypothesis: '',
variants: [ variants: [
{ key: 'control', name: 'Control', description: 'Current implementation', isControl: true, flagConfig: {} }, {
key: 'control',
name: 'Control',
description: 'Current implementation',
isControl: true,
flagConfig: {},
},
{ key: 'variant_a', name: 'Variant A', description: '', isControl: false, flagConfig: {} }, { key: 'variant_a', name: 'Variant A', description: '', isControl: false, flagConfig: {} },
], ],
allocationStrategy: 'random', allocationStrategy: 'random',
@ -160,8 +178,8 @@ export default function NewExperimentPage() {
isActive isActive
? 'bg-primary text-white' ? 'bg-primary text-white'
: isCompleted : isCompleted
? 'bg-green-100 text-green-600' ? 'bg-green-100 text-green-600'
: 'bg-muted' : 'bg-muted'
}`} }`}
> >
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
@ -191,18 +209,10 @@ export default function NewExperimentPage() {
onApplySuggestion={applyAiSuggestion} onApplySuggestion={applyAiSuggestion}
/> />
)} )}
{currentStep === 1 && ( {currentStep === 1 && <VariantsStep formData={formData} setFormData={setFormData} />}
<VariantsStep formData={formData} setFormData={setFormData} /> {currentStep === 2 && <MetricsStep formData={formData} setFormData={setFormData} />}
)} {currentStep === 3 && <TargetingStep formData={formData} setFormData={setFormData} />}
{currentStep === 2 && ( {currentStep === 4 && <ReviewStep formData={formData} />}
<MetricsStep formData={formData} setFormData={setFormData} />
)}
{currentStep === 3 && (
<TargetingStep formData={formData} setFormData={setFormData} />
)}
{currentStep === 4 && (
<ReviewStep formData={formData} />
)}
</CardContent> </CardContent>
</Card> </Card>
@ -312,9 +322,15 @@ function HypothesisStep({
{aiSuggestions.length > 0 ? ( {aiSuggestions.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{aiSuggestions.map((suggestion, index) => ( {aiSuggestions.map((suggestion, index) => (
<Card key={index} className="cursor-pointer hover:border-primary" onClick={() => onApplySuggestion(suggestion)}> <Card
key={index}
className="cursor-pointer hover:border-primary"
onClick={() => onApplySuggestion(suggestion)}
>
<CardContent className="p-3"> <CardContent className="p-3">
<p className="text-sm font-medium line-clamp-2">{suggestion.hypothesis.primary}</p> <p className="text-sm font-medium line-clamp-2">
{suggestion.hypothesis.primary}
</p>
<div className="flex gap-2 mt-2"> <div className="flex gap-2 mt-2">
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
Impact: {suggestion.hypothesis.impactScore}/100 Impact: {suggestion.hypothesis.impactScore}/100
@ -329,7 +345,8 @@ function HypothesisStep({
</div> </div>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Click "Load Suggestions" to see AI-generated experiment ideas based on your product usage patterns. Click &quot;Load Suggestions&quot; to see AI-generated experiment ideas based on your
product usage patterns.
</p> </p>
)} )}
</div> </div>
@ -383,7 +400,9 @@ function VariantsStep({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{variant.isControl ? ( {variant.isControl ? (
<Badge variant="outline" className="border-blue-300 text-blue-700">Control</Badge> <Badge variant="outline" className="border-blue-300 text-blue-700">
Control
</Badge>
) : ( ) : (
<Badge variant="outline">Variant</Badge> <Badge variant="outline">Variant</Badge>
)} )}
@ -429,7 +448,8 @@ function VariantsStep({
))} ))}
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
You need at least 2 variants: a Control (current implementation) and at least one Treatment variant. You need at least 2 variants: a Control (current implementation) and at least one Treatment
variant.
</p> </p>
</div> </div>
); );
@ -488,7 +508,7 @@ function MetricsStep({
onValueChange={v => onValueChange={v =>
setFormData({ setFormData({
...formData, ...formData,
primaryMetric: { ...formData.primaryMetric!, type: v as any }, primaryMetric: { ...formData.primaryMetric!, type: v as MetricType },
}) })
} }
> >
@ -511,7 +531,10 @@ function MetricsStep({
onValueChange={v => onValueChange={v =>
setFormData({ setFormData({
...formData, ...formData,
primaryMetric: { ...formData.primaryMetric!, direction: v as any }, primaryMetric: {
...formData.primaryMetric!,
direction: v as PrimaryMetric['direction'],
},
}) })
} }
> >
@ -536,7 +559,9 @@ function MetricsStep({
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<Select <Select
value={formData.allocationStrategy} value={formData.allocationStrategy}
onValueChange={v => setFormData({ ...formData, allocationStrategy: v as any })} onValueChange={v =>
setFormData({ ...formData, allocationStrategy: v as AllocationStrategy })
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@ -590,7 +615,9 @@ function TargetingStep({
{['ios', 'android', 'web'].map(platform => ( {['ios', 'android', 'web'].map(platform => (
<Badge <Badge
key={platform} key={platform}
variant={formData.targeting?.platforms?.includes(platform) ? 'default' : 'outline'} variant={
formData.targeting?.platforms?.includes(platform) ? 'default' : 'outline'
}
className="cursor-pointer capitalize" className="cursor-pointer capitalize"
onClick={() => { onClick={() => {
const current = formData.targeting?.platforms || []; const current = formData.targeting?.platforms || [];
@ -615,7 +642,9 @@ function TargetingStep({
{['free', 'pro', 'enterprise'].map(segment => ( {['free', 'pro', 'enterprise'].map(segment => (
<Badge <Badge
key={segment} key={segment}
variant={formData.targeting?.userSegments?.includes(segment) ? 'default' : 'outline'} variant={
formData.targeting?.userSegments?.includes(segment) ? 'default' : 'outline'
}
className="cursor-pointer capitalize" className="cursor-pointer capitalize"
onClick={() => { onClick={() => {
const current = formData.targeting?.userSegments || []; const current = formData.targeting?.userSegments || [];
@ -668,7 +697,10 @@ function TargetingStep({
onChange={e => onChange={e =>
setFormData({ setFormData({
...formData, ...formData,
guardrails: { ...formData.guardrails!, maxDurationDays: parseInt(e.target.value) }, guardrails: {
...formData.guardrails!,
maxDurationDays: parseInt(e.target.value),
},
}) })
} }
className="mt-1" className="mt-1"
@ -686,7 +718,10 @@ function TargetingStep({
onChange={e => onChange={e =>
setFormData({ setFormData({
...formData, ...formData,
guardrails: { ...formData.guardrails!, winnerThreshold: parseInt(e.target.value) }, guardrails: {
...formData.guardrails!,
winnerThreshold: parseInt(e.target.value),
},
}) })
} }
className="mt-1" className="mt-1"
@ -713,7 +748,10 @@ function ReviewStep({ formData }: { formData: Partial<CreateExperimentInput> })
<ReviewItem label="Primary Metric" value={formData.primaryMetric?.name} /> <ReviewItem label="Primary Metric" value={formData.primaryMetric?.name} />
<ReviewItem label="Allocation Strategy" value={formData.allocationStrategy} /> <ReviewItem label="Allocation Strategy" value={formData.allocationStrategy} />
<ReviewItem label="Target Traffic" value={`${formData.targetPercent}%`} /> <ReviewItem label="Target Traffic" value={`${formData.targetPercent}%`} />
<ReviewItem label="Auto Stop" value={formData.guardrails?.autoStopEnabled ? 'Enabled' : 'Disabled'} /> <ReviewItem
label="Auto Stop"
value={formData.guardrails?.autoStopEnabled ? 'Enabled' : 'Disabled'}
/>
</div> </div>
<Alert> <Alert>

View File

@ -40,6 +40,7 @@ export default function ExperimentsPage() {
const [experiments, setExperiments] = useState<ExperimentDoc[]>([]); const [experiments, setExperiments] = useState<ExperimentDoc[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [now] = useState(() => Date.now());
useEffect(() => { useEffect(() => {
fetchExperiments(); fetchExperiments();
@ -113,9 +114,7 @@ export default function ExperimentsPage() {
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground"> <CardTitle className="text-sm font-medium text-muted-foreground">Running</CardTitle>
Running
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-3xl font-bold text-green-600">{runningCount}</div> <div className="text-3xl font-bold text-green-600">{runningCount}</div>
@ -123,9 +122,7 @@ export default function ExperimentsPage() {
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground"> <CardTitle className="text-sm font-medium text-muted-foreground">Completed</CardTitle>
Completed
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-3xl font-bold text-blue-600">{completedCount}</div> <div className="text-3xl font-bold text-blue-600">{completedCount}</div>
@ -149,12 +146,14 @@ export default function ExperimentsPage() {
<TabsTrigger value="all">All ({experiments.length})</TabsTrigger> <TabsTrigger value="all">All ({experiments.length})</TabsTrigger>
<TabsTrigger value="running">Running ({runningCount})</TabsTrigger> <TabsTrigger value="running">Running ({runningCount})</TabsTrigger>
<TabsTrigger value="completed">Completed ({completedCount})</TabsTrigger> <TabsTrigger value="completed">Completed ({completedCount})</TabsTrigger>
<TabsTrigger value="draft">Drafts ({experiments.filter(e => e.status === 'draft').length})</TabsTrigger> <TabsTrigger value="draft">
Drafts ({experiments.filter(e => e.status === 'draft').length})
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="all" className="space-y-4"> <TabsContent value="all" className="space-y-4">
{experiments.map(experiment => ( {experiments.map(experiment => (
<ExperimentCard key={experiment.id} experiment={experiment} /> <ExperimentCard key={experiment.id} experiment={experiment} now={now} />
))} ))}
{experiments.length === 0 && ( {experiments.length === 0 && (
<div className="text-center py-12 text-muted-foreground"> <div className="text-center py-12 text-muted-foreground">
@ -168,7 +167,7 @@ export default function ExperimentsPage() {
{experiments {experiments
.filter(e => e.status === 'running') .filter(e => e.status === 'running')
.map(experiment => ( .map(experiment => (
<ExperimentCard key={experiment.id} experiment={experiment} /> <ExperimentCard key={experiment.id} experiment={experiment} now={now} />
))} ))}
</TabsContent> </TabsContent>
@ -176,7 +175,7 @@ export default function ExperimentsPage() {
{experiments {experiments
.filter(e => e.status === 'completed') .filter(e => e.status === 'completed')
.map(experiment => ( .map(experiment => (
<ExperimentCard key={experiment.id} experiment={experiment} /> <ExperimentCard key={experiment.id} experiment={experiment} now={now} />
))} ))}
</TabsContent> </TabsContent>
@ -184,7 +183,7 @@ export default function ExperimentsPage() {
{experiments {experiments
.filter(e => e.status === 'draft') .filter(e => e.status === 'draft')
.map(experiment => ( .map(experiment => (
<ExperimentCard key={experiment.id} experiment={experiment} /> <ExperimentCard key={experiment.id} experiment={experiment} now={now} />
))} ))}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
@ -192,12 +191,12 @@ export default function ExperimentsPage() {
); );
} }
function ExperimentCard({ experiment }: { experiment: ExperimentDoc }) { function ExperimentCard({ experiment, now }: { experiment: ExperimentDoc; now: number }) {
const status = statusConfig[experiment.status] || statusConfig.draft; const status = statusConfig[experiment.status] || statusConfig.draft;
const StatusIcon = status.icon; const StatusIcon = status.icon;
const daysRunning = experiment.startedAt const daysRunning = experiment.startedAt
? Math.floor((Date.now() - new Date(experiment.startedAt).getTime()) / (1000 * 60 * 60 * 24)) ? Math.floor((now - new Date(experiment.startedAt).getTime()) / (1000 * 60 * 60 * 24))
: 0; : 0;
return ( return (

View File

@ -1,23 +1,22 @@
import * as React from "react" import * as React from 'react';
import { cn } from "@/lib/utils" import { cn } from '@/lib/utils';
export interface TextareaProps export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
className={cn( className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
Textarea.displayName = "Textarea" Textarea.displayName = 'Textarea';
export { Textarea } export { Textarea };