From 6bbc4e63c618df0624b1d92cc6a3207c32cd7009 Mon Sep 17 00:00:00 2001 From: Joker Date: Fri, 4 Sep 2026 03:41:22 +0000 Subject: [PATCH] style(frontend): prettier pass post-codemod MUI 9 (card 7db80121) --- dashboard/frontend/e2e/app.spec.ts | 82 +-- dashboard/frontend/e2e/smoke.spec.ts | 92 ++-- dashboard/frontend/playwright.config.ts | 20 +- .../src/__tests__/routes.smoke.test.tsx | 75 +-- .../frontend/src/components/ErrorBoundary.tsx | 46 +- .../src/components/OnboardingWizard.tsx | 512 ++++++++++-------- .../achievements/AchievementBadge.tsx | 85 +-- .../achievements/AchievementsList.tsx | 38 +- .../src/components/billing/InvoiceList.tsx | 70 ++- .../components/billing/PaymentMethodForm.tsx | 100 ++-- .../src/components/billing/PlanCard.tsx | 70 +-- .../components/billing/SubscriptionStatus.tsx | 153 ++++-- .../src/components/common/EmptyState.tsx | 69 +-- .../common/KeyboardShortcutsDialog.tsx | 60 +- .../src/components/common/SkeletonLoader.tsx | 116 ++-- .../components/dashboard/TimeSavedCard.tsx | 56 +- .../components/dashboard/TokenUsageChart.tsx | 117 ++-- .../src/components/feedback/FeedbackForm.tsx | 279 ++++++---- .../src/components/filters/AdvancedFilter.tsx | 161 +++--- .../integrations/IntegrationCard.tsx | 216 +++++--- .../notifications/NotificationDropdown.tsx | 283 +++++----- dashboard/frontend/src/main.tsx | 38 +- dashboard/frontend/src/pages/Analytics.tsx | 319 +++++++---- dashboard/frontend/src/pages/Billing.tsx | 372 +++++++------ dashboard/frontend/src/pages/Dashboard.tsx | 326 ++++++----- .../frontend/src/pages/DashboardEnhanced.tsx | 381 +++++++------ dashboard/frontend/src/pages/Executions.tsx | 141 +++-- .../frontend/src/pages/ForgotPassword.tsx | 112 ++-- dashboard/frontend/src/pages/Integrations.tsx | 104 ++-- dashboard/frontend/src/pages/Landing.tsx | 442 ++++++++++----- dashboard/frontend/src/pages/Login.tsx | 162 +++--- dashboard/frontend/src/pages/NotFound.tsx | 48 +- dashboard/frontend/src/pages/Profile.tsx | 29 +- dashboard/frontend/src/pages/Register.tsx | 281 +++++----- dashboard/frontend/src/pages/SelfHealing.tsx | 202 ++++--- dashboard/frontend/src/pages/Settings.tsx | 237 ++++---- dashboard/frontend/src/pages/TestCases.tsx | 217 +++++--- dashboard/frontend/src/pages/TestSuites.tsx | 201 ++++--- 38 files changed, 3716 insertions(+), 2596 deletions(-) diff --git a/dashboard/frontend/e2e/app.spec.ts b/dashboard/frontend/e2e/app.spec.ts index e433671..dce7ebe 100644 --- a/dashboard/frontend/e2e/app.spec.ts +++ b/dashboard/frontend/e2e/app.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; // Card 4eb58505: no hardcoded external targets. // - Frontend navigations are relative and resolve against the playwright @@ -17,29 +17,28 @@ import { test, expect } from '@playwright/test'; // - E2E_ENV=production flips the docs test to expect 404: the backend // disables /api/v1/docs when settings.is_production (main.py, PR #147), // and asserting that response is the point of the test in prod. -const BACKEND_URL = process.env.E2E_API_URL ?? ''; -const hasApi = BACKEND_URL !== ''; +const BACKEND_URL = process.env.E2E_API_URL ?? ""; +const hasApi = BACKEND_URL !== ""; -const E2E_USERNAME = process.env.E2E_AUTH_USERNAME ?? ''; -const E2E_PASSWORD = process.env.E2E_AUTH_PASSWORD ?? ''; -const hasAuth = E2E_USERNAME !== '' && E2E_PASSWORD !== ''; +const E2E_USERNAME = process.env.E2E_AUTH_USERNAME ?? ""; +const E2E_PASSWORD = process.env.E2E_AUTH_PASSWORD ?? ""; +const hasAuth = E2E_USERNAME !== "" && E2E_PASSWORD !== ""; const AUTH_SKIP_REASON = - 'E2E_AUTH_USERNAME / E2E_AUTH_PASSWORD not set — configure them (CI secrets or local env) to run login tests against this environment'; + "E2E_AUTH_USERNAME / E2E_AUTH_PASSWORD not set — configure them (CI secrets or local env) to run login tests against this environment"; -const IS_PRODUCTION = (process.env.E2E_ENV ?? '') === 'production'; +const IS_PRODUCTION = (process.env.E2E_ENV ?? "") === "production"; -test.describe('QA-FRAMEWORK E2E Tests', () => { - - test('Backend health check', async ({ request }) => { - test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend'); +test.describe("QA-FRAMEWORK E2E Tests", () => { + test("Backend health check", async ({ request }) => { + test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend"); const response = await request.get(`${BACKEND_URL}/health`); expect(response.ok()).toBeTruthy(); const data = await response.json(); - expect(data.status).toBe('healthy'); + expect(data.status).toBe("healthy"); }); - test('Backend API docs accessible', async ({ request }) => { - test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend'); + test("Backend API docs accessible", async ({ request }) => { + test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend"); const response = await request.get(`${BACKEND_URL}/api/v1/docs`); if (IS_PRODUCTION) { // Production deliberately disables the docs endpoints @@ -50,60 +49,63 @@ test.describe('QA-FRAMEWORK E2E Tests', () => { } }); - test('Login API works', async ({ request }) => { - test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend'); + test("Login API works", async ({ request }) => { + test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend"); test.skip(!hasAuth, AUTH_SKIP_REASON); const response = await request.post(`${BACKEND_URL}/api/v1/auth/login`, { - headers: { 'Content-Type': 'application/json' }, - data: { username: E2E_USERNAME, password: E2E_PASSWORD } + headers: { "Content-Type": "application/json" }, + data: { username: E2E_USERNAME, password: E2E_PASSWORD }, }); expect(response.ok()).toBeTruthy(); const data = await response.json(); expect(data.access_token).toBeDefined(); - expect(data.token_type).toBe('bearer'); + expect(data.token_type).toBe("bearer"); }); - test('Get user info with token', async ({ request }) => { - test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend'); + test("Get user info with token", async ({ request }) => { + test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend"); test.skip(!hasAuth, AUTH_SKIP_REASON); // First login - const loginResponse = await request.post(`${BACKEND_URL}/api/v1/auth/login`, { - headers: { 'Content-Type': 'application/json' }, - data: { username: E2E_USERNAME, password: E2E_PASSWORD } - }); + const loginResponse = await request.post( + `${BACKEND_URL}/api/v1/auth/login`, + { + headers: { "Content-Type": "application/json" }, + data: { username: E2E_USERNAME, password: E2E_PASSWORD }, + }, + ); const loginData = await loginResponse.json(); const token = loginData.access_token; // Get user info const meResponse = await request.get(`${BACKEND_URL}/api/v1/me`, { - headers: { 'Authorization': `Bearer ${token}` } + headers: { Authorization: `Bearer ${token}` }, }); expect(meResponse.ok()).toBeTruthy(); const userData = await meResponse.json(); expect(userData.username).toBe(E2E_USERNAME); }); - test('Frontend loads', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('body')).toBeVisible(); + test("Frontend loads", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("body")).toBeVisible(); }); - test('Login page displays correctly', async ({ page }) => { - await page.goto('/login'); + test("Login page displays correctly", async ({ page }) => { + await page.goto("/login"); // Wait for page to load - await page.waitForLoadState('networkidle'); + await page.waitForLoadState("networkidle"); // Check for login form elements - const usernameInput = page.locator('input').first(); + const usernameInput = page.locator("input").first(); await expect(usernameInput).toBeVisible(); }); - test('Full login flow', async ({ page }) => { + test("Full login flow", async ({ page }) => { test.skip(!hasAuth, AUTH_SKIP_REASON); - await page.goto('/login'); - await page.waitForLoadState('networkidle'); + await page.goto("/login"); + await page.waitForLoadState("networkidle"); // Fill login form using placeholder or type - const inputs = page.locator('input'); + const inputs = page.locator("input"); await inputs.nth(0).fill(E2E_USERNAME); await inputs.nth(1).fill(E2E_PASSWORD); @@ -119,8 +121,8 @@ test.describe('QA-FRAMEWORK E2E Tests', () => { expect(url).toBeDefined(); }); - test('Billing plans accessible without auth', async ({ request }) => { - test.skip(!hasApi, 'E2E_API_URL not set — requires a deployed backend'); + test("Billing plans accessible without auth", async ({ request }) => { + test.skip(!hasApi, "E2E_API_URL not set — requires a deployed backend"); const response = await request.get(`${BACKEND_URL}/api/v1/billing/plans`); expect(response.ok()).toBeTruthy(); const data = await response.json(); diff --git a/dashboard/frontend/e2e/smoke.spec.ts b/dashboard/frontend/e2e/smoke.spec.ts index 8306c39..74ba1bc 100644 --- a/dashboard/frontend/e2e/smoke.spec.ts +++ b/dashboard/frontend/e2e/smoke.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; /** * Regression smoke suite (card 4eb58505, from QA's PR #212 local smoke). @@ -8,46 +8,64 @@ import { test, expect } from '@playwright/test'; * Catches runtime breakage of the majors migration in a real browser: * uncaught exceptions, broken public routes, broken auth redirect. */ -test.describe('Smoke regression (post-migration)', () => { - test('Landing renders with content', async ({ page }) => { +test.describe("Smoke regression (post-migration)", () => { + test("Landing renders with content", async ({ page }) => { const errors: string[] = []; - page.on('pageerror', (e) => errors.push(String(e))); - await page.goto('/'); - await expect(page.locator('body')).toBeVisible(); - await expect(page.locator('main, [role="main"], h1, h2').first()).toBeVisible(); - expect(errors, `uncaught JS errors on /: ${errors.join(' | ')}`).toEqual([]); + page.on("pageerror", (e) => errors.push(String(e))); + await page.goto("/"); + await expect(page.locator("body")).toBeVisible(); + await expect( + page.locator('main, [role="main"], h1, h2').first(), + ).toBeVisible(); + expect(errors, `uncaught JS errors on /: ${errors.join(" | ")}`).toEqual( + [], + ); }); - test('Login renders form (2 inputs + submit)', async ({ page }) => { + test("Login renders form (2 inputs + submit)", async ({ page }) => { const errors: string[] = []; - page.on('pageerror', (e) => errors.push(String(e))); - await page.goto('/login'); - await expect(page.locator('input').first()).toBeVisible(); - const inputs = page.locator('input'); + page.on("pageerror", (e) => errors.push(String(e))); + await page.goto("/login"); + await expect(page.locator("input").first()).toBeVisible(); + const inputs = page.locator("input"); expect(await inputs.count()).toBeGreaterThanOrEqual(2); - await expect(page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Sign")').first()).toBeVisible(); - expect(errors, `uncaught JS errors on /login: ${errors.join(' | ')}`).toEqual([]); + await expect( + page + .locator( + 'button[type="submit"], button:has-text("Login"), button:has-text("Sign")', + ) + .first(), + ).toBeVisible(); + expect( + errors, + `uncaught JS errors on /login: ${errors.join(" | ")}`, + ).toEqual([]); }); - test('Register renders form', async ({ page }) => { + test("Register renders form", async ({ page }) => { const errors: string[] = []; - page.on('pageerror', (e) => errors.push(String(e))); - await page.goto('/register'); - await expect(page.locator('input').first()).toBeVisible(); - expect(await page.locator('input').count()).toBeGreaterThanOrEqual(2); - expect(errors, `uncaught JS errors on /register: ${errors.join(' | ')}`).toEqual([]); + page.on("pageerror", (e) => errors.push(String(e))); + await page.goto("/register"); + await expect(page.locator("input").first()).toBeVisible(); + expect(await page.locator("input").count()).toBeGreaterThanOrEqual(2); + expect( + errors, + `uncaught JS errors on /register: ${errors.join(" | ")}`, + ).toEqual([]); }); - test('Unauthenticated /dashboard redirects to /login (RR7)', async ({ page }) => { - await page.goto('/dashboard'); + test("Unauthenticated /dashboard redirects to /login (RR7)", async ({ + page, + }) => { + await page.goto("/dashboard"); await page.waitForURL(/\/login/, { timeout: 10_000 }); - expect(page.url()).toContain('/login'); + expect(page.url()).toContain("/login"); }); - test('No 404/500 on unknown route (NotFound page)', async ({ page }) => { + test("No 404/500 on unknown route (NotFound page)", async ({ page }) => { const errors: string[] = []; - page.on('pageerror', (e) => errors.push(String(e))); - const resp = await page.goto('/definitely-not-a-route'); + page.on("pageerror", (e) => errors.push(String(e))); + const resp = await page.goto("/definitely-not-a-route"); expect(resp?.status()).toBeLessThan(500); expect(errors).toEqual([]); }); @@ -55,13 +73,19 @@ test.describe('Smoke regression (post-migration)', () => { // Visual evidence for the codemod that removed margin="normal" from // TextField (PR #212, reviewer 9a74c5b6): full-page screenshots are // uploaded as CI artifacts from every run. - test('Visual spot-check: Login & Register screenshots', async ({ page }) => { - await page.goto('/login'); - await expect(page.locator('input').first()).toBeVisible(); - await page.screenshot({ path: 'test-results/spotcheck-login.png', fullPage: true }); + test("Visual spot-check: Login & Register screenshots", async ({ page }) => { + await page.goto("/login"); + await expect(page.locator("input").first()).toBeVisible(); + await page.screenshot({ + path: "test-results/spotcheck-login.png", + fullPage: true, + }); - await page.goto('/register'); - await expect(page.locator('input').first()).toBeVisible(); - await page.screenshot({ path: 'test-results/spotcheck-register.png', fullPage: true }); + await page.goto("/register"); + await expect(page.locator("input").first()).toBeVisible(); + await page.screenshot({ + path: "test-results/spotcheck-register.png", + fullPage: true, + }); }); }); diff --git a/dashboard/frontend/playwright.config.ts b/dashboard/frontend/playwright.config.ts index a3945f5..c609efd 100644 --- a/dashboard/frontend/playwright.config.ts +++ b/dashboard/frontend/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; // Card 4eb58505: E2E targets are env-driven — zero hardcoded external stacks. // - CI (PR): pr-deploy-coolify.yml injects E2E_BASE_URL pointing at the @@ -10,30 +10,30 @@ import { defineConfig, devices } from '@playwright/test'; const E2E_BASE_URL = process.env.E2E_BASE_URL; export default defineConfig({ - testDir: './e2e', + testDir: "./e2e", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, - reporter: 'html', + reporter: "html", use: { - baseURL: E2E_BASE_URL ?? 'http://localhost:4173', - trace: 'on-first-retry', - screenshot: 'only-on-failure', + baseURL: E2E_BASE_URL ?? "http://localhost:4173", + trace: "on-first-retry", + screenshot: "only-on-failure", // no `video`: requires the ffmpeg playwright binary — not worth the CI // dependency for this suite (screenshots + trace carry the evidence). }, projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, ], webServer: E2E_BASE_URL ? undefined : { - command: 'npm run build && npm run preview -- --port 4173 --strictPort', - url: 'http://localhost:4173', + command: "npm run build && npm run preview -- --port 4173 --strictPort", + url: "http://localhost:4173", reuseExistingServer: !process.env.CI, timeout: 180_000, }, diff --git a/dashboard/frontend/src/__tests__/routes.smoke.test.tsx b/dashboard/frontend/src/__tests__/routes.smoke.test.tsx index d520d92..4c0c6d3 100644 --- a/dashboard/frontend/src/__tests__/routes.smoke.test.tsx +++ b/dashboard/frontend/src/__tests__/routes.smoke.test.tsx @@ -1,46 +1,49 @@ -import { describe, it, expect } from 'vitest' -import { render } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from 'react-query' -import App from '../App' +import { describe, it, expect } from "vitest"; +import { render } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "react-query"; +import App from "../App"; -const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) -const renderApp = () => render( - - - -) +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}); +const renderApp = () => + render( + + + , + ); /** * Route smoke tests for the MUI 5 -> 9 / react-router 6 -> 7 migration. * Verifies that public routes still render their pages after the upgrade. */ -describe('Route smoke (post-migration)', () => { - it('renders Landing at /', () => { - window.history.pushState({}, '', '/') - const { unmount } = renderApp() - expect(document.body.innerHTML.length).toBeGreaterThan(0) - unmount() - }) +describe("Route smoke (post-migration)", () => { + it("renders Landing at /", () => { + window.history.pushState({}, "", "/"); + const { unmount } = renderApp(); + expect(document.body.innerHTML.length).toBeGreaterThan(0); + unmount(); + }); - it('renders Login at /login', () => { - window.history.pushState({}, '', '/login') - const { unmount } = renderApp() - expect(document.body.innerHTML.length).toBeGreaterThan(0) - unmount() - }) + it("renders Login at /login", () => { + window.history.pushState({}, "", "/login"); + const { unmount } = renderApp(); + expect(document.body.innerHTML.length).toBeGreaterThan(0); + unmount(); + }); - it('renders Register at /register', () => { - window.history.pushState({}, '', '/register') - const { unmount } = renderApp() - expect(document.body.innerHTML.length).toBeGreaterThan(0) - unmount() - }) + it("renders Register at /register", () => { + window.history.pushState({}, "", "/register"); + const { unmount } = renderApp(); + expect(document.body.innerHTML.length).toBeGreaterThan(0); + unmount(); + }); - it('unauthenticated users are redirected away from /dashboard', () => { - window.history.pushState({}, '', '/dashboard') - const { unmount } = renderApp() + it("unauthenticated users are redirected away from /dashboard", () => { + window.history.pushState({}, "", "/dashboard"); + const { unmount } = renderApp(); // ProtectedRoute redirects to /login when not authenticated - expect(window.location.pathname).toBe('/login') - unmount() - }) -}) + expect(window.location.pathname).toBe("/login"); + unmount(); + }); +}); diff --git a/dashboard/frontend/src/components/ErrorBoundary.tsx b/dashboard/frontend/src/components/ErrorBoundary.tsx index 0f56543..3249d8c 100644 --- a/dashboard/frontend/src/components/ErrorBoundary.tsx +++ b/dashboard/frontend/src/components/ErrorBoundary.tsx @@ -1,49 +1,51 @@ -import React, { Component, ErrorInfo, ReactNode } from 'react' -import { Box, Typography, Button } from '@mui/material' +import React, { Component, ErrorInfo, ReactNode } from "react"; +import { Box, Typography, Button } from "@mui/material"; interface Props { - children: ReactNode + children: ReactNode; } interface State { - hasError: boolean - error?: Error + hasError: boolean; + error?: Error; } class ErrorBoundary extends Component { public state: State = { - hasError: false - } + hasError: false, + }; public static getDerivedStateFromError(error: Error): State { - return { hasError: true, error } + return { hasError: true, error }; } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('Error caught by boundary:', error, errorInfo) + console.error("Error caught by boundary:", error, errorInfo); } private handleReset = () => { - this.setState({ hasError: false, error: undefined }) - window.location.href = '/' - } + this.setState({ hasError: false, error: undefined }); + window.location.href = "/"; + }; public render() { if (this.state.hasError) { return ( Something went wrong - {this.state.error?.message || 'An unexpected error occurred'} + {this.state.error?.message || "An unexpected error occurred"} - ) + ); } - return this.props.children + return this.props.children; } } -export default ErrorBoundary \ No newline at end of file +export default ErrorBoundary; diff --git a/dashboard/frontend/src/components/OnboardingWizard.tsx b/dashboard/frontend/src/components/OnboardingWizard.tsx index ea98c27..cd4a406 100644 --- a/dashboard/frontend/src/components/OnboardingWizard.tsx +++ b/dashboard/frontend/src/components/OnboardingWizard.tsx @@ -13,8 +13,8 @@ * State persists across page refreshes. */ -import React, { useState, useEffect, useCallback } from 'react' -import { useNavigate } from 'react-router-dom' +import React, { useState, useEffect, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; import { Stepper, Step, @@ -28,7 +28,7 @@ import { Alert, IconButton, Tooltip, -} from '@mui/material' +} from "@mui/material"; import { Check as CheckIcon, Close as CloseIcon, @@ -38,124 +38,142 @@ import { RocketLaunch as RocketIcon, Assignment as SuiteIcon, SkipNext as SkipNextIcon, -} from '@mui/icons-material' -import { useMutation, useQuery } from 'react-query' -import { onboardingAPI, suitesAPI, executionsAPI } from '../api/client' -import useAuthStore from '../stores/authStore' -import toast from 'react-hot-toast' +} from "@mui/icons-material"; +import { useMutation, useQuery } from "react-query"; +import { onboardingAPI, suitesAPI, executionsAPI } from "../api/client"; +import useAuthStore from "../stores/authStore"; +import toast from "react-hot-toast"; const STEPS = [ - { key: 'welcome', label: 'Welcome', icon: }, - { key: 'connect_repo', label: 'Connect Repo', icon: }, - { key: 'create_suite', label: 'Create Suite', icon: }, - { key: 'run_test', label: 'Run Test', icon: }, - { key: 'setup_notifications', label: 'Notifications', icon: }, -] + { key: "welcome", label: "Welcome", icon: }, + { key: "connect_repo", label: "Connect Repo", icon: }, + { key: "create_suite", label: "Create Suite", icon: }, + { key: "run_test", label: "Run Test", icon: }, + { + key: "setup_notifications", + label: "Notifications", + icon: , + }, +]; interface OnboardingWizardProps { - onComplete: () => void + onComplete: () => void; } -export const OnboardingWizard: React.FC = ({ onComplete }) => { - const [activeStep, setActiveStep] = useState(0) - const [completedSteps, setCompletedSteps] = useState>({}) - const navigate = useNavigate() - const { setNeedsOnboarding } = useAuthStore() +export const OnboardingWizard: React.FC = ({ + onComplete, +}) => { + const [activeStep, setActiveStep] = useState(0); + const [completedSteps, setCompletedSteps] = useState>( + {}, + ); + const navigate = useNavigate(); + const { setNeedsOnboarding } = useAuthStore(); // Fetch current onboarding state from backend const { data: onboardingState, isLoading } = useQuery( - 'onboarding-state', + "onboarding-state", () => onboardingAPI.getState(), { onSuccess: (response) => { - const state = response.data + const state = response.data; if (state.completed) { - onComplete() - return + onComplete(); + return; } - setActiveStep(state.current_step || 0) + setActiveStep(state.current_step || 0); if (state.steps) { - setCompletedSteps(state.steps) + setCompletedSteps(state.steps); } }, onError: () => { - toast.error('Failed to load onboarding state') + toast.error("Failed to load onboarding state"); }, - } - ) + }, + ); // Mutation to update a step const stepMutation = useMutation( (stepName: string) => onboardingAPI.updateStep(stepName, true), { onSuccess: (_, stepName) => { - setCompletedSteps((prev) => ({ ...prev, [stepName]: true })) + setCompletedSteps((prev) => ({ ...prev, [stepName]: true })); // Auto-advance to next incomplete step - const currentIndex = STEPS.findIndex((s) => s.key === stepName) - let nextStep = currentIndex + 1 + const currentIndex = STEPS.findIndex((s) => s.key === stepName); + let nextStep = currentIndex + 1; while (nextStep < STEPS.length && completedSteps[STEPS[nextStep].key]) { - nextStep++ + nextStep++; } if (nextStep < STEPS.length) { - setActiveStep(nextStep) + setActiveStep(nextStep); } else { // All steps done, complete onboarding - completeMutation.mutate() + completeMutation.mutate(); } - toast.success(`Step "${STEPS.find(s => s.key === stepName)?.label}" completed!`) + toast.success( + `Step "${STEPS.find((s) => s.key === stepName)?.label}" completed!`, + ); }, onError: () => { - toast.error('Failed to save progress') + toast.error("Failed to save progress"); }, - } - ) + }, + ); // Mutation to complete onboarding - const completeMutation = useMutation( - () => onboardingAPI.complete(), - { - onSuccess: () => { - setNeedsOnboarding(false) - toast.success('Onboarding complete! Welcome aboard! 🎉') - onComplete() - }, - onError: () => { - toast.error('Failed to complete onboarding') - }, - } - ) + const completeMutation = useMutation(() => onboardingAPI.complete(), { + onSuccess: () => { + setNeedsOnboarding(false); + toast.success("Onboarding complete! Welcome aboard! 🎉"); + onComplete(); + }, + onError: () => { + toast.error("Failed to complete onboarding"); + }, + }); // Mutation to skip onboarding - const skipMutation = useMutation( - () => onboardingAPI.skip(), - { - onSuccess: () => { - setNeedsOnboarding(false) - toast.success('Onboarding skipped. You can find help in Settings.') - onComplete() - }, - onError: () => { - toast.error('Failed to skip onboarding') - }, - } - ) + const skipMutation = useMutation(() => onboardingAPI.skip(), { + onSuccess: () => { + setNeedsOnboarding(false); + toast.success("Onboarding skipped. You can find help in Settings."); + onComplete(); + }, + onError: () => { + toast.error("Failed to skip onboarding"); + }, + }); const handleSkip = useCallback(() => { - skipMutation.mutate() - }, [skipMutation]) + skipMutation.mutate(); + }, [skipMutation]); if (isLoading) { return ( - + - ) + ); } return ( - + {/* Header with skip option */} - + Welcome to QA-FRAMEWORK 🚀 @@ -173,25 +191,29 @@ export const OnboardingWizard: React.FC = ({ onComplete } slotProps={{ stepIcon: { component: () => ( - - {completedSteps[step.key] ? : step.icon} - + + {completedSteps[step.key] ? ( + + ) : ( + step.icon + )} + ), }, }} @@ -206,12 +228,10 @@ export const OnboardingWizard: React.FC = ({ onComplete } {activeStep < STEPS.length ? ( <> - {renderStepContent( - activeStep, - stepMutation, - completedSteps, - )} - + {renderStepContent(activeStep, stepMutation, completedSteps)} + ) : ( - + 🎉 Setup Complete! @@ -252,8 +276,8 @@ export const OnboardingWizard: React.FC = ({ onComplete } - ) -} + ); +}; // --- Step Components --- @@ -264,17 +288,21 @@ function renderStepContent( ) { switch (step) { case 0: - return + return ; case 1: - return + return ; case 2: - return stepMutation.mutate('create_suite')} /> + return ( + stepMutation.mutate("create_suite")} + /> + ); case 3: - return stepMutation.mutate('run_test')} /> + return stepMutation.mutate("run_test")} />; case 4: - return + return ; default: - return null + return null; } } @@ -287,25 +315,55 @@ const WelcomeStep: React.FC = () => ( In just a few steps, you'll have your first test suite up and running. Here's what we'll set up: - + {[ - { icon: , title: 'Connect Repository', desc: 'Link your GitHub repo for CI/CD integration' }, - { icon: , title: 'Create Test Suite', desc: 'Set up your first automated test collection' }, - { icon: , title: 'Run First Test', desc: 'Execute a test and see the results' }, - { icon: , title: 'Notifications', desc: 'Configure alerts for test results' }, + { + icon: , + title: "Connect Repository", + desc: "Link your GitHub repo for CI/CD integration", + }, + { + icon: , + title: "Create Test Suite", + desc: "Set up your first automated test collection", + }, + { + icon: , + title: "Run First Test", + desc: "Execute a test and see the results", + }, + { + icon: , + title: "Notifications", + desc: "Configure alerts for test results", + }, ].map((item) => ( - + {item.icon} - {item.title} - {item.desc} + + {item.title} + + + {item.desc} + ))} -) +); const ConnectRepoStep: React.FC = () => { - const [repoUrl, setRepoUrl] = useState('') + const [repoUrl, setRepoUrl] = useState(""); return ( @@ -313,8 +371,8 @@ const ConnectRepoStep: React.FC = () => { Connect Your Repository - Enter your GitHub repository URL to enable CI/CD integration. - You can also connect via OAuth later in Settings. + Enter your GitHub repository URL to enable CI/CD integration. You can + also connect via OAuth later in Settings. @@ -323,49 +381,62 @@ const ConnectRepoStep: React.FC = () => { type="text" placeholder="https://github.com/username/repo" value={repoUrl} - onChange={(e: React.ChangeEvent) => setRepoUrl(e.target.value)} + onChange={(e: React.ChangeEvent) => + setRepoUrl(e.target.value) + } sx={{ - width: '100%', + width: "100%", p: 2, - fontSize: '1rem', - border: '1px solid', - borderColor: 'divider', + fontSize: "1rem", + border: "1px solid", + borderColor: "divider", borderRadius: 1, - bgcolor: 'background.paper', - outline: 'none', - '&:focus': { borderColor: 'primary.main', boxShadow: '0 0 0 2px rgba(25, 118, 210, 0.2)' }, + bgcolor: "background.paper", + outline: "none", + "&:focus": { + borderColor: "primary.main", + boxShadow: "0 0 0 2px rgba(25, 118, 210, 0.2)", + }, }} /> - This step is optional. You can connect a repository later from the Integrations page. + This step is optional. You can connect a repository later from the + Integrations page. - ) -} + ); +}; -const CreateSuiteStep: React.FC<{ onComplete: () => void }> = ({ onComplete }) => { - const [suiteName, setSuiteName] = useState('My First Test Suite') - const [suiteDesc, setSuiteDesc] = useState('Automated smoke tests for critical paths') +const CreateSuiteStep: React.FC<{ onComplete: () => void }> = ({ + onComplete, +}) => { + const [suiteName, setSuiteName] = useState("My First Test Suite"); + const [suiteDesc, setSuiteDesc] = useState( + "Automated smoke tests for critical paths", + ); const createMutation = useMutation( - () => suitesAPI.create({ - name: suiteName, - description: suiteDesc, - framework_type: 'pytest', - config: {}, - }), + () => + suitesAPI.create({ + name: suiteName, + description: suiteDesc, + framework_type: "pytest", + config: {}, + }), { onSuccess: () => { - toast.success('Test suite created!') - onComplete() + toast.success("Test suite created!"); + onComplete(); }, onError: (error: any) => { - toast.error(error.response?.data?.detail || 'Failed to create test suite') + toast.error( + error.response?.data?.detail || "Failed to create test suite", + ); }, - } - ) + }, + ); return ( @@ -381,37 +452,41 @@ const CreateSuiteStep: React.FC<{ onComplete: () => void }> = ({ onComplete }) = type="text" placeholder="Suite name" value={suiteName} - onChange={(e: React.ChangeEvent) => setSuiteName(e.target.value)} + onChange={(e: React.ChangeEvent) => + setSuiteName(e.target.value) + } sx={{ - width: '100%', + width: "100%", p: 2, mb: 2, - fontSize: '1rem', - border: '1px solid', - borderColor: 'divider', + fontSize: "1rem", + border: "1px solid", + borderColor: "divider", borderRadius: 1, - bgcolor: 'background.paper', - outline: 'none', - '&:focus': { borderColor: 'primary.main' }, + bgcolor: "background.paper", + outline: "none", + "&:focus": { borderColor: "primary.main" }, }} /> ) => setSuiteDesc(e.target.value)} + onChange={(e: React.ChangeEvent) => + setSuiteDesc(e.target.value) + } rows={3} sx={{ - width: '100%', + width: "100%", p: 2, - fontSize: '1rem', - border: '1px solid', - borderColor: 'divider', + fontSize: "1rem", + border: "1px solid", + borderColor: "divider", borderRadius: 1, - bgcolor: 'background.paper', - outline: 'none', - resize: 'vertical', - '&:focus': { borderColor: 'primary.main' }, + bgcolor: "background.paper", + outline: "none", + resize: "vertical", + "&:focus": { borderColor: "primary.main" }, }} /> @@ -420,18 +495,20 @@ const CreateSuiteStep: React.FC<{ onComplete: () => void }> = ({ onComplete }) = onClick={() => createMutation.mutate()} disabled={!suiteName.trim() || createMutation.isLoading} sx={{ mt: 2 }} - endIcon={createMutation.isLoading ? : null} + endIcon={ + createMutation.isLoading ? : null + } > - {createMutation.isLoading ? 'Creating...' : 'Create Suite'} + {createMutation.isLoading ? "Creating..." : "Create Suite"} - ) -} + ); +}; const RunTestStep: React.FC<{ onComplete: () => void }> = ({ onComplete }) => { const [testCode, setTestCode] = useState( - `def test_homepage_loads():\n response = client.get('/')\n assert response.status_code == 200` - ) + `def test_homepage_loads():\n response = client.get('/')\n assert response.status_code == 200`, + ); return ( @@ -439,51 +516,48 @@ const RunTestStep: React.FC<{ onComplete: () => void }> = ({ onComplete }) => { Run Your First Test - Here's a sample test. You'll be able to create more complex tests - once you're set up. + Here's a sample test. You'll be able to create more complex tests once + you're set up. {testCode} - ✓ Test execution will be available once you've created a test suite and added test cases. - For now, this step is marked as complete to get you started quickly. + ✓ Test execution will be available once you've created a test suite and + added test cases. For now, this step is marked as complete to get you + started quickly. - - ) -} + ); +}; const SetupNotificationsStep: React.FC = () => { const [channels, setChannels] = useState({ email: true, slack: false, discord: false, - }) + }); const toggleChannel = (channel: keyof typeof channels) => { - setChannels((prev) => ({ ...prev, [channel]: !prev[channel] })) - } + setChannels((prev) => ({ ...prev, [channel]: !prev[channel] })); + }; return ( @@ -491,57 +565,71 @@ const SetupNotificationsStep: React.FC = () => { Configure Notifications - Get notified when tests complete, fail, or need attention. - You can always change these later in Settings. + Get notified when tests complete, fail, or need attention. You can + always change these later in Settings. - + {[ - { key: 'email' as const, label: 'Email Notifications', desc: 'Receive test results via email' }, - { key: 'slack' as const, label: 'Slack Integration', desc: 'Post results to a Slack channel' }, - { key: 'discord' as const, label: 'Discord Integration', desc: 'Send alerts to Discord' }, + { + key: "email" as const, + label: "Email Notifications", + desc: "Receive test results via email", + }, + { + key: "slack" as const, + label: "Slack Integration", + desc: "Post results to a Slack channel", + }, + { + key: "discord" as const, + label: "Discord Integration", + desc: "Send alerts to Discord", + }, ].map((item) => ( toggleChannel(item.key)} sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', + display: "flex", + alignItems: "center", + justifyContent: "space-between", p: 2, - border: '1px solid', - borderColor: channels[item.key] ? 'primary.main' : 'divider', + border: "1px solid", + borderColor: channels[item.key] ? "primary.main" : "divider", borderRadius: 2, - cursor: 'pointer', - bgcolor: channels[item.key] ? 'primary.50' : 'transparent', - transition: 'all 0.2s', - '&:hover': { borderColor: 'primary.light' }, + cursor: "pointer", + bgcolor: channels[item.key] ? "primary.50" : "transparent", + transition: "all 0.2s", + "&:hover": { borderColor: "primary.light" }, }} > {item.label} - {item.desc} + + {item.desc} + @@ -549,7 +637,7 @@ const SetupNotificationsStep: React.FC = () => { ))} - ) -} + ); +}; -export default OnboardingWizard +export default OnboardingWizard; diff --git a/dashboard/frontend/src/components/achievements/AchievementBadge.tsx b/dashboard/frontend/src/components/achievements/AchievementBadge.tsx index 3e10487..6ac7e15 100644 --- a/dashboard/frontend/src/components/achievements/AchievementBadge.tsx +++ b/dashboard/frontend/src/components/achievements/AchievementBadge.tsx @@ -1,19 +1,19 @@ -import { Box, Typography, Paper, Tooltip, LinearProgress } from '@mui/material'; -import { Achievement } from '../../types/achievements'; -import { useAchievementsStore } from '../../stores/achievementsStore'; +import { Box, Typography, Paper, Tooltip, LinearProgress } from "@mui/material"; +import { Achievement } from "../../types/achievements"; +import { useAchievementsStore } from "../../stores/achievementsStore"; interface AchievementBadgeProps { achievement: Achievement; - size?: 'small' | 'medium' | 'large'; + size?: "small" | "medium" | "large"; showProgress?: boolean; } const RARITY_COLORS = { - common: '#9E9E9E', - uncommon: '#4CAF50', - rare: '#2196F3', - epic: '#9C27B0', - legendary: '#FF9800', + common: "#9E9E9E", + uncommon: "#4CAF50", + rare: "#2196F3", + epic: "#9C27B0", + legendary: "#FF9800", }; const SIZE_CONFIG = { @@ -24,7 +24,7 @@ const SIZE_CONFIG = { export default function AchievementBadge({ achievement, - size = 'medium', + size = "medium", showProgress = true, }: AchievementBadgeProps) { const { isUnlocked, getProgress } = useAchievementsStore(); @@ -42,7 +42,7 @@ export default function AchievementBadge({ {achievement.name} {achievement.description} - + Points: {achievement.points} | Rarity: {achievement.rarity} {!unlocked && progress > 0 && ( @@ -59,21 +59,21 @@ export default function AchievementBadge({ sx={{ width: config.width, height: config.height, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - border: `3px solid ${unlocked ? rarityColor : '#E0E0E0'}`, + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + position: "relative", + border: `3px solid ${unlocked ? rarityColor : "#E0E0E0"}`, borderRadius: 2, background: unlocked ? `linear-gradient(135deg, ${rarityColor}22 0%, ${rarityColor}44 100%)` - : '#F5F5F5', + : "#F5F5F5", opacity: unlocked ? 1 : 0.5, - transition: 'all 0.3s ease', - cursor: 'pointer', - '&:hover': { - transform: 'scale(1.05)', + transition: "all 0.3s ease", + cursor: "pointer", + "&:hover": { + transform: "scale(1.05)", boxShadow: unlocked ? 6 : 2, }, }} @@ -82,7 +82,7 @@ export default function AchievementBadge({ @@ -91,29 +91,32 @@ export default function AchievementBadge({ {/* Name */} {achievement.name} {/* Progress Bar */} {showProgress && !unlocked && progress > 0 && ( - + ✓ diff --git a/dashboard/frontend/src/components/achievements/AchievementsList.tsx b/dashboard/frontend/src/components/achievements/AchievementsList.tsx index 37ddfab..36c777b 100644 --- a/dashboard/frontend/src/components/achievements/AchievementsList.tsx +++ b/dashboard/frontend/src/components/achievements/AchievementsList.tsx @@ -1,27 +1,27 @@ -import { Box, Typography, Grid, Tabs, Tab, Chip } from '@mui/material'; -import { useState } from 'react'; -import AchievementBadge from './AchievementBadge'; -import { ACHIEVEMENTS } from '../../data/achievements'; -import { useAchievementsStore } from '../../stores/achievementsStore'; -import { AchievementCategory } from '../../types/achievements'; +import { Box, Typography, Grid, Tabs, Tab, Chip } from "@mui/material"; +import { useState } from "react"; +import AchievementBadge from "./AchievementBadge"; +import { ACHIEVEMENTS } from "../../data/achievements"; +import { useAchievementsStore } from "../../stores/achievementsStore"; +import { AchievementCategory } from "../../types/achievements"; -const CATEGORIES: { value: AchievementCategory | 'all'; label: string }[] = [ - { value: 'all', label: 'All' }, - { value: 'testing', label: 'Testing' }, - { value: 'automation', label: 'Automation' }, - { value: 'quality', label: 'Quality' }, - { value: 'speed', label: 'Speed' }, - { value: 'dedication', label: 'Dedication' }, - { value: 'special', label: 'Special' }, +const CATEGORIES: { value: AchievementCategory | "all"; label: string }[] = [ + { value: "all", label: "All" }, + { value: "testing", label: "Testing" }, + { value: "automation", label: "Automation" }, + { value: "quality", label: "Quality" }, + { value: "speed", label: "Speed" }, + { value: "dedication", label: "Dedication" }, + { value: "special", label: "Special" }, ]; export default function AchievementsList() { - const [category, setCategory] = useState('all'); + const [category, setCategory] = useState("all"); const { getStats } = useAchievementsStore(); const stats = getStats(); const filteredAchievements = - category === 'all' + category === "all" ? ACHIEVEMENTS : ACHIEVEMENTS.filter((a) => a.category === category); @@ -32,7 +32,7 @@ export default function AchievementsList() { Your Achievements - + setCategory(newValue)} variant="scrollable" scrollButtons="auto" - sx={{ mb: 3, borderBottom: 1, borderColor: 'divider' }} + sx={{ mb: 3, borderBottom: 1, borderColor: "divider" }} > {CATEGORIES.map((cat) => ( @@ -70,7 +70,7 @@ export default function AchievementsList() { {/* Empty State */} {filteredAchievements.length === 0 && ( - + No achievements in this category yet diff --git a/dashboard/frontend/src/components/billing/InvoiceList.tsx b/dashboard/frontend/src/components/billing/InvoiceList.tsx index ddf8a33..76b0d90 100644 --- a/dashboard/frontend/src/components/billing/InvoiceList.tsx +++ b/dashboard/frontend/src/components/billing/InvoiceList.tsx @@ -12,67 +12,77 @@ import { IconButton, Box, CircularProgress, -} from '@mui/material' +} from "@mui/material"; import { Download as DownloadIcon, Receipt as ReceiptIcon, -} from '@mui/icons-material' -import { format } from 'date-fns' +} from "@mui/icons-material"; +import { format } from "date-fns"; interface Invoice { - id: string - number: string - amount: number - currency: string - status: 'paid' | 'open' | 'void' | 'uncollectible' - created_at: string - due_date?: string - invoice_url?: string + id: string; + number: string; + amount: number; + currency: string; + status: "paid" | "open" | "void" | "uncollectible"; + created_at: string; + due_date?: string; + invoice_url?: string; } interface InvoiceListProps { - invoices: Invoice[] - isLoading?: boolean + invoices: Invoice[]; + isLoading?: boolean; } -const statusColors: Record = { - paid: 'success', - open: 'warning', - void: 'error', - uncollectible: 'error', -} +const statusColors: Record< + string, + "success" | "warning" | "error" | "default" +> = { + paid: "success", + open: "warning", + void: "error", + uncollectible: "error", +}; export default function InvoiceList({ invoices, isLoading }: InvoiceListProps) { const formatAmount = (amount: number, currency: string) => { - return new Intl.NumberFormat('en-US', { - style: 'currency', + return new Intl.NumberFormat("en-US", { + style: "currency", currency: currency.toUpperCase(), - }).format(amount / 100) - } + }).format(amount / 100); + }; if (isLoading) { return ( - + - ) + ); } if (invoices.length === 0) { return ( - - + + No invoices yet - ) + ); } return ( @@ -112,7 +122,7 @@ export default function InvoiceList({ invoices, isLoading }: InvoiceListProps) { /> - {format(new Date(invoice.created_at), 'MMM d, yyyy')} + {format(new Date(invoice.created_at), "MMM d, yyyy")} {invoice.invoice_url && ( @@ -133,5 +143,5 @@ export default function InvoiceList({ invoices, isLoading }: InvoiceListProps) { - ) + ); } diff --git a/dashboard/frontend/src/components/billing/PaymentMethodForm.tsx b/dashboard/frontend/src/components/billing/PaymentMethodForm.tsx index 6b1ec99..64e67ea 100644 --- a/dashboard/frontend/src/components/billing/PaymentMethodForm.tsx +++ b/dashboard/frontend/src/components/billing/PaymentMethodForm.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState } from "react"; import { Dialog, DialogTitle, @@ -11,69 +11,73 @@ import { CircularProgress, TextField, Grid, -} from '@mui/material' +} from "@mui/material"; import { CreditCard as CreditCardIcon, Add as AddIcon, -} from '@mui/icons-material' +} from "@mui/icons-material"; interface PaymentMethodFormProps { - open: boolean - onClose: () => void - onSubmit: (paymentMethodId: string) => Promise + open: boolean; + onClose: () => void; + onSubmit: (paymentMethodId: string) => Promise; } // Note: In production, you would use Stripe Elements for secure card input // This is a simplified version for demonstration -export default function PaymentMethodForm({ open, onClose, onSubmit }: PaymentMethodFormProps) { - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [cardNumber, setCardNumber] = useState('') - const [expiry, setExpiry] = useState('') - const [cvc, setCvc] = useState('') +export default function PaymentMethodForm({ + open, + onClose, + onSubmit, +}: PaymentMethodFormProps) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [cardNumber, setCardNumber] = useState(""); + const [expiry, setExpiry] = useState(""); + const [cvc, setCvc] = useState(""); const handleSubmit = async () => { - setLoading(true) - setError(null) + setLoading(true); + setError(null); try { // In production, you would: // 1. Use Stripe.js to create a payment method // 2. Send only the payment method ID to your server // This is a placeholder implementation - const mockPaymentMethodId = `pm_${Date.now()}` - await onSubmit(mockPaymentMethodId) - onClose() + const mockPaymentMethodId = `pm_${Date.now()}`; + await onSubmit(mockPaymentMethodId); + onClose(); } catch (err: any) { - setError(err.response?.data?.detail || 'Failed to add payment method') + setError(err.response?.data?.detail || "Failed to add payment method"); } finally { - setLoading(false) + setLoading(false); } - } + }; const formatCardNumber = (value: string) => { - const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '') - const matches = v.match(/\d{4,16}/g) - const match = (matches && matches[0]) || '' - const parts = [] + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + const matches = v.match(/\d{4,16}/g); + const match = (matches && matches[0]) || ""; + const parts = []; for (let i = 0, len = match.length; i < len; i += 4) { - parts.push(match.substring(i, i + 4)) + parts.push(match.substring(i, i + 4)); } - return parts.length ? parts.join(' ') : value - } + return parts.length ? parts.join(" ") : value; + }; const formatExpiry = (value: string) => { - const v = value.replace(/\s+/g, '').replace(/[^0-9]/gi, '') + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); if (v.length >= 2) { - return v.slice(0, 2) + '/' + v.slice(2, 4) + return v.slice(0, 2) + "/" + v.slice(2, 4); } - return v - } + return v; + }; return ( - + Add Payment Method @@ -88,28 +92,34 @@ export default function PaymentMethodForm({ open, onClose, onSubmit }: PaymentMe - For production, this form would use Stripe Elements for secure card input. - This is a demonstration version. + For production, this form would use Stripe Elements for secure card + input. This is a demonstration version. - + setCardNumber(formatCardNumber(e.target.value))} - slotProps={{ htmlInput: { maxLength: 19 }, input: { - startAdornment: , - } }} - + onChange={(e) => + setCardNumber(formatCardNumber(e.target.value)) + } + slotProps={{ + htmlInput: { maxLength: 19 }, + input: { + startAdornment: ( + + ), + }, + }} /> - + - + setCvc(e.target.value.replace(/\D/g, '').slice(0, 4))} + onChange={(e) => + setCvc(e.target.value.replace(/\D/g, "").slice(0, 4)) + } slotProps={{ htmlInput: { maxLength: 4 } }} /> @@ -148,5 +160,5 @@ export default function PaymentMethodForm({ open, onClose, onSubmit }: PaymentMe - ) + ); } diff --git a/dashboard/frontend/src/components/billing/PlanCard.tsx b/dashboard/frontend/src/components/billing/PlanCard.tsx index 24ac382..9fad906 100644 --- a/dashboard/frontend/src/components/billing/PlanCard.tsx +++ b/dashboard/frontend/src/components/billing/PlanCard.tsx @@ -9,52 +9,52 @@ import { ListItem, ListItemIcon, ListItemText, -} from '@mui/material' +} from "@mui/material"; import { Check as CheckIcon, Close as CloseIcon, Star as StarIcon, -} from '@mui/icons-material' +} from "@mui/icons-material"; interface PlanFeature { - name: string - included: boolean + name: string; + included: boolean; } interface Plan { - id: string - name: string - price: number - interval: 'month' | 'year' - features: PlanFeature[] - popular?: boolean - current?: boolean + id: string; + name: string; + price: number; + interval: "month" | "year"; + features: PlanFeature[]; + popular?: boolean; + current?: boolean; } interface PlanCardProps { - plan: Plan - onSelect: (planId: string) => void - isLoading?: boolean + plan: Plan; + onSelect: (planId: string) => void; + isLoading?: boolean; } export default function PlanCard({ plan, onSelect, isLoading }: PlanCardProps) { const formatPrice = (price: number) => { - if (price === 0) return 'Free' - return `$${price}/${plan.interval}` - } + if (price === 0) return "Free"; + return `$${price}/${plan.interval}`; + }; return ( ))} @@ -111,15 +113,19 @@ export default function PlanCard({ plan, onSelect, isLoading }: PlanCardProps) { - ) + ); } diff --git a/dashboard/frontend/src/components/billing/SubscriptionStatus.tsx b/dashboard/frontend/src/components/billing/SubscriptionStatus.tsx index d0118d8..9f7c1ba 100644 --- a/dashboard/frontend/src/components/billing/SubscriptionStatus.tsx +++ b/dashboard/frontend/src/components/billing/SubscriptionStatus.tsx @@ -8,49 +8,52 @@ import { Grid, Divider, LinearProgress, -} from '@mui/material' +} from "@mui/material"; import { CheckCircle as CheckCircleIcon, Warning as WarningIcon, Error as ErrorIcon, Cancel as CancelIcon, -} from '@mui/icons-material' -import { format } from 'date-fns' +} from "@mui/icons-material"; +import { format } from "date-fns"; interface Subscription { - id: string - plan_id: string - plan_name: string - status: 'active' | 'past_due' | 'canceled' | 'incomplete' | 'trialing' - current_period_start: string - current_period_end: string - cancel_at_period_end: boolean + id: string; + plan_id: string; + plan_name: string; + status: "active" | "past_due" | "canceled" | "incomplete" | "trialing"; + current_period_start: string; + current_period_end: string; + cancel_at_period_end: boolean; features: { - max_suites: number - max_cases: number - ai_healing: boolean - priority_support: boolean - } + max_suites: number; + max_cases: number; + ai_healing: boolean; + priority_support: boolean; + }; usage?: { - suites_used: number - cases_used: number - } + suites_used: number; + cases_used: number; + }; } interface SubscriptionStatusProps { - subscription: Subscription | null - onCancel: () => void - onUpgrade: () => void - isLoading?: boolean + subscription: Subscription | null; + onCancel: () => void; + onUpgrade: () => void; + isLoading?: boolean; } -const statusConfig: Record = { - active: { icon: CheckCircleIcon, color: 'success', label: 'Active' }, - past_due: { icon: WarningIcon, color: 'warning', label: 'Past Due' }, - canceled: { icon: CancelIcon, color: 'error', label: 'Canceled' }, - incomplete: { icon: ErrorIcon, color: 'error', label: 'Incomplete' }, - trialing: { icon: CheckCircleIcon, color: 'info', label: 'Trialing' }, -} +const statusConfig: Record< + string, + { icon: typeof CheckCircleIcon; color: string; label: string } +> = { + active: { icon: CheckCircleIcon, color: "success", label: "Active" }, + past_due: { icon: WarningIcon, color: "warning", label: "Past Due" }, + canceled: { icon: CancelIcon, color: "error", label: "Canceled" }, + incomplete: { icon: ErrorIcon, color: "error", label: "Incomplete" }, + trialing: { icon: CheckCircleIcon, color: "info", label: "Trialing" }, +}; export default function SubscriptionStatus({ subscription, @@ -62,40 +65,51 @@ export default function SubscriptionStatus({ return ( - + No Active Subscription You are currently on the Free plan - - ) + ); } - const StatusIcon = statusConfig[subscription.status]?.icon || ErrorIcon - const statusColor = statusConfig[subscription.status]?.color || 'error' - const statusLabel = statusConfig[subscription.status]?.label || 'Unknown' + const StatusIcon = statusConfig[subscription.status]?.icon || ErrorIcon; + const statusColor = statusConfig[subscription.status]?.color || "error"; + const statusLabel = statusConfig[subscription.status]?.label || "Unknown"; const periodProgress = () => { - if (!subscription.current_period_start || !subscription.current_period_end) return 0 - const start = new Date(subscription.current_period_start).getTime() - const end = new Date(subscription.current_period_end).getTime() - const now = Date.now() - return Math.min(100, Math.max(0, ((now - start) / (end - start)) * 100)) - } + if (!subscription.current_period_start || !subscription.current_period_end) + return 0; + const start = new Date(subscription.current_period_start).getTime(); + const end = new Date(subscription.current_period_end).getTime(); + const now = Date.now(); + return Math.min(100, Math.max(0, ((now - start) / (end - start)) * 100)); + }; return ( - - + + {subscription.plan_name} @@ -112,8 +126,12 @@ export default function SubscriptionStatus({ Current billing period - {format(new Date(subscription.current_period_start), 'MMM d, yyyy')} -{' '} - {format(new Date(subscription.current_period_end), 'MMM d, yyyy')} + {format( + new Date(subscription.current_period_start), + "MMM d, yyyy", + )}{" "} + -{" "} + {format(new Date(subscription.current_period_end), "MMM d, yyyy")} @@ -129,45 +147,68 @@ export default function SubscriptionStatus({ sx={{ mt: 2, p: 2, - bgcolor: 'warning.lighter', + bgcolor: "warning.lighter", borderRadius: 1, }} > - Your subscription will be canceled at the end of the current billing period + Your subscription will be canceled at the end of the current + billing period )} - + Usage This Period {subscription.usage && ( - + Test Suites - {subscription.usage.suites_used}/{subscription.features.max_suites} + {subscription.usage.suites_used}/ + {subscription.features.max_suites} - + Test Cases - {subscription.usage.cases_used}/{subscription.features.max_cases} + {subscription.usage.cases_used}/ + {subscription.features.max_cases} @@ -175,7 +216,7 @@ export default function SubscriptionStatus({ - + - - {SUGGESTED_TAGS.filter(t => !formData.tags.includes(t)).slice(0, 6).map((tag) => ( - handleAddTag(tag)} - size="small" - variant="outlined" - sx={{ cursor: 'pointer' }} - /> - ))} + + {SUGGESTED_TAGS.filter((t) => !formData.tags.includes(t)) + .slice(0, 6) + .map((tag) => ( + handleAddTag(tag)} + size="small" + variant="outlined" + sx={{ cursor: "pointer" }} + /> + ))} @@ -325,12 +372,12 @@ export default function FeedbackForm({ onSuccess, compact = false }: FeedbackFor startIcon={} sx={{ mt: 2 }} > - {submitMutation.isLoading ? 'Submitting...' : 'Submit Feedback'} + {submitMutation.isLoading ? "Submitting..." : "Submit Feedback"} - ) + ); } diff --git a/dashboard/frontend/src/components/filters/AdvancedFilter.tsx b/dashboard/frontend/src/components/filters/AdvancedFilter.tsx index 1b65a10..e7a6c58 100644 --- a/dashboard/frontend/src/components/filters/AdvancedFilter.tsx +++ b/dashboard/frontend/src/components/filters/AdvancedFilter.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback } from "react"; import { Box, TextField, @@ -11,28 +11,23 @@ import { Collapse, Button, Typography, -} from '@mui/material' -import { - FilterList, - Close, - Save, - Search, -} from '@mui/icons-material' +} from "@mui/material"; +import { FilterList, Close, Save, Search } from "@mui/icons-material"; export interface FilterConfig { - search?: string - status?: string - environment?: string - testType?: string - dateFrom?: string - dateTo?: string - tags?: string[] + search?: string; + status?: string; + environment?: string; + testType?: string; + dateFrom?: string; + dateTo?: string; + tags?: string[]; } interface AdvancedFilterProps { - onFilterChange: (filters: FilterConfig) => void - savedFilters?: { name: string; config: FilterConfig }[] - onSaveFilter?: (name: string, config: FilterConfig) => void + onFilterChange: (filters: FilterConfig) => void; + savedFilters?: { name: string; config: FilterConfig }[]; + onSaveFilter?: (name: string, config: FilterConfig) => void; } export default function AdvancedFilter({ @@ -40,78 +35,86 @@ export default function AdvancedFilter({ savedFilters = [], onSaveFilter, }: AdvancedFilterProps) { - const [expanded, setExpanded] = useState(false) - const [filters, setFilters] = useState({}) - const [tagInput, setTagInput] = useState('') - const [presetName, setPresetName] = useState('') + const [expanded, setExpanded] = useState(false); + const [filters, setFilters] = useState({}); + const [tagInput, setTagInput] = useState(""); + const [presetName, setPresetName] = useState(""); // Debounced search useEffect(() => { const timer = setTimeout(() => { - onFilterChange(filters) - }, 300) - return () => clearTimeout(timer) - }, [filters, onFilterChange]) + onFilterChange(filters); + }, 300); + return () => clearTimeout(timer); + }, [filters, onFilterChange]); - const handleFilterChange = useCallback((key: keyof FilterConfig, value: any) => { - setFilters(prev => ({ ...prev, [key]: value })) - }, []) + const handleFilterChange = useCallback( + (key: keyof FilterConfig, value: any) => { + setFilters((prev) => ({ ...prev, [key]: value })); + }, + [], + ); const handleAddTag = () => { if (tagInput.trim() && !filters.tags?.includes(tagInput.trim())) { - setFilters(prev => ({ + setFilters((prev) => ({ ...prev, tags: [...(prev.tags || []), tagInput.trim()], - })) - setTagInput('') + })); + setTagInput(""); } - } + }; const handleRemoveTag = (tag: string) => { - setFilters(prev => ({ + setFilters((prev) => ({ ...prev, - tags: prev.tags?.filter(t => t !== tag), - })) - } + tags: prev.tags?.filter((t) => t !== tag), + })); + }; const handleSavePreset = () => { if (presetName.trim() && onSaveFilter) { - onSaveFilter(presetName.trim(), filters) - setPresetName('') + onSaveFilter(presetName.trim(), filters); + setPresetName(""); } - } + }; const handleLoadPreset = (config: FilterConfig) => { - setFilters(config) - } + setFilters(config); + }; const handleClearFilters = () => { - setFilters({}) - } + setFilters({}); + }; - const activeFilterCount = Object.values(filters).filter(v => - v !== undefined && v !== '' && (Array.isArray(v) ? v.length > 0 : true) - ).length + const activeFilterCount = Object.values(filters).filter( + (v) => + v !== undefined && v !== "" && (Array.isArray(v) ? v.length > 0 : true), + ).length; return ( - + setExpanded(!expanded)}> handleFilterChange('search', e.target.value)} - slotProps={{ input: { - startAdornment: , - } }} + value={filters.search || ""} + onChange={(e) => handleFilterChange("search", e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + ), + }, + }} sx={{ flexGrow: 1, maxWidth: 400 }} /> {activeFilterCount > 0 && ( 1 ? 's' : ''} active`} + label={`${activeFilterCount} filter${activeFilterCount > 1 ? "s" : ""} active`} color="primary" size="small" onDelete={handleClearFilters} @@ -120,15 +123,15 @@ export default function AdvancedFilter({ - + - + Status handleFilterChange('environment', e.target.value)} + onChange={(e) => + handleFilterChange("environment", e.target.value) + } > All Development @@ -155,13 +160,15 @@ export default function AdvancedFilter({ - + Test Type setFormData({ ...formData, test_type: e.target.value })} + onChange={(e) => + setFormData({ ...formData, test_type: e.target.value }) + } > API Testing UI Testing @@ -291,7 +326,9 @@ export default function TestCases() { Priority