├── .gitignore
├── generated-icon.png
├── postcss.config.js
├── theme.json
├── client
├── src
│ ├── components
│ │ ├── ui
│ │ │ ├── aspect-ratio.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── label.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── input.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── resizable.tsx
│ │ │ ├── toggle-group.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── button.tsx
│ │ │ ├── accordion.tsx
│ │ │ ├── card.tsx
│ │ │ ├── input-otp.tsx
│ │ │ ├── calendar.tsx
│ │ │ ├── breadcrumb.tsx
│ │ │ ├── pagination.tsx
│ │ │ ├── table.tsx
│ │ │ ├── drawer.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── form.tsx
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── command.tsx
│ │ │ ├── navigation-menu.tsx
│ │ │ ├── select.tsx
│ │ │ ├── carousel.tsx
│ │ │ ├── context-menu.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── menubar.tsx
│ │ │ └── chart.tsx
│ │ ├── tasks
│ │ │ ├── status-chart.tsx
│ │ │ ├── task-list.tsx
│ │ │ └── task-form.tsx
│ │ └── api-test
│ │ │ ├── response-viewer.tsx
│ │ │ └── request-builder.tsx
│ ├── lib
│ │ ├── utils.ts
│ │ ├── queryClient.ts
│ │ ├── websocket.ts
│ │ └── api.ts
│ ├── index.css
│ ├── main.tsx
│ ├── hooks
│ │ ├── use-mobile.tsx
│ │ └── use-toast.ts
│ ├── pages
│ │ ├── not-found.tsx
│ │ ├── test-dashboard.tsx
│ │ ├── dashboard.tsx
│ │ └── metrics-dashboard.tsx
│ └── App.tsx
└── index.html
├── drizzle.config.ts
├── db
├── index.ts
└── schema.ts
├── tsconfig.json
├── vite.config.ts
├── .replit
├── LICENSE
├── server
├── index.ts
├── vite.ts
└── routes.ts
├── tailwind.config.ts
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .DS_Store
4 | server/public
5 | vite.config.ts.*
6 | *.tar.gz
--------------------------------------------------------------------------------
/generated-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yoheinakajima/pippin-tasks/HEAD/generated-icon.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "variant": "professional",
3 | "primary": "hsl(222.2 47.4% 11.2%)",
4 | "appearance": "light",
5 | "radius": 0.5
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2 |
3 | const AspectRatio = AspectRatioPrimitive.Root
4 |
5 | export { AspectRatio }
6 |
--------------------------------------------------------------------------------
/client/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | * {
7 | @apply border-border;
8 | }
9 |
10 | body {
11 | @apply font-sans antialiased bg-background text-foreground;
12 | }
13 | }
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/client/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | const Collapsible = CollapsiblePrimitive.Root
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
10 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "drizzle-kit";
2 |
3 | if (!process.env.DATABASE_URL) {
4 | throw new Error("DATABASE_URL, ensure the database is provisioned");
5 | }
6 |
7 | export default defineConfig({
8 | out: "./migrations",
9 | schema: "./db/schema.ts",
10 | dialect: "postgresql",
11 | dbCredentials: {
12 | url: process.env.DATABASE_URL,
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/db/index.ts:
--------------------------------------------------------------------------------
1 | import { drizzle } from "drizzle-orm/neon-serverless";
2 | import ws from "ws";
3 | import * as schema from "@db/schema";
4 |
5 | if (!process.env.DATABASE_URL) {
6 | throw new Error(
7 | "DATABASE_URL must be set. Did you forget to provision a database?",
8 | );
9 | }
10 |
11 | export const db = drizzle({
12 | connection: process.env.DATABASE_URL,
13 | schema,
14 | ws: ws,
15 | });
16 |
--------------------------------------------------------------------------------
/client/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import { QueryClientProvider } from "@tanstack/react-query";
4 | import { queryClient } from "./lib/queryClient";
5 | import App from './App';
6 | import "./index.css";
7 |
8 | const root = document.getElementById("root");
9 | if (!root) {
10 | throw new Error("Root element not found");
11 | }
12 |
13 | createRoot(root).render(
14 |
15 |
16 |
17 |
18 |
19 | );
--------------------------------------------------------------------------------
/client/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["client/src/**/*", "db/**/*", "server/**/*"],
3 | "exclude": ["node_modules", "build", "dist", "**/*.test.ts"],
4 | "compilerOptions": {
5 | "incremental": true,
6 | "tsBuildInfoFile": "./node_modules/typescript/tsbuildinfo",
7 | "noEmit": true,
8 | "module": "ESNext",
9 | "strict": true,
10 | "lib": ["esnext", "dom", "dom.iterable"],
11 | "jsx": "preserve",
12 | "esModuleInterop": true,
13 | "skipLibCheck": true,
14 | "allowImportingTsExtensions": true,
15 | "moduleResolution": "bundler",
16 | "baseUrl": ".",
17 | "types": ["node", "vite/client"],
18 | "paths": {
19 | "@db": ["./db/index.ts"],
20 | "@db/*": ["./db/*"],
21 | "@/*": ["./client/src/*"]
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/pages/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "@/components/ui/card";
2 | import { AlertCircle } from "lucide-react";
3 |
4 | export default function NotFound() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
404 Page Not Found
12 |
13 |
14 |
15 | Did you forget to add the page to the router?
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/client/src/lib/queryClient.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from "@tanstack/react-query";
2 |
3 | export const queryClient = new QueryClient({
4 | defaultOptions: {
5 | queries: {
6 | queryFn: async ({ queryKey }) => {
7 | const res = await fetch(queryKey[0] as string, {
8 | credentials: "include",
9 | });
10 |
11 | if (!res.ok) {
12 | if (res.status >= 500) {
13 | throw new Error(`${res.status}: ${res.statusText}`);
14 | }
15 |
16 | throw new Error(`${res.status}: ${await res.text()}`);
17 | }
18 |
19 | return res.json();
20 | },
21 | refetchInterval: false,
22 | refetchOnWindowFocus: false,
23 | staleTime: Infinity,
24 | retry: false,
25 | },
26 | mutations: {
27 | retry: false,
28 | }
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import themePlugin from "@replit/vite-plugin-shadcn-theme-json";
4 | import path, { dirname } from "path";
5 | import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
6 | import { fileURLToPath } from "url";
7 |
8 | const __filename = fileURLToPath(import.meta.url);
9 | const __dirname = dirname(__filename);
10 | export default defineConfig({
11 | plugins: [react(), runtimeErrorOverlay(), themePlugin()],
12 | resolve: {
13 | alias: {
14 | "@db": path.resolve(__dirname, "db"),
15 | "@": path.resolve(__dirname, "client", "src"),
16 | },
17 | },
18 | root: path.resolve(__dirname, "client"),
19 | build: {
20 | outDir: path.resolve(__dirname, "dist/public"),
21 | emptyOutDir: true,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/client/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/client/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/client/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/client/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import { useToast } from "@/hooks/use-toast"
2 | import {
3 | Toast,
4 | ToastClose,
5 | ToastDescription,
6 | ToastProvider,
7 | ToastTitle,
8 | ToastViewport,
9 | } from "@/components/ui/toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title}}
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Switch, Route } from "wouter";
2 | import { QueryClientProvider } from "@tanstack/react-query";
3 | import { queryClient } from "./lib/queryClient";
4 | import { Toaster } from "@/components/ui/toaster";
5 | import Dashboard from "@/pages/dashboard";
6 | import TestDashboard from "@/pages/test-dashboard";
7 | import MetricsDashboard from "@/pages/metrics-dashboard";
8 | import NotFound from "@/pages/not-found";
9 |
10 | function Router() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | function App() {
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default App;
--------------------------------------------------------------------------------
/client/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/.replit:
--------------------------------------------------------------------------------
1 | modules = ["nodejs-20", "web", "postgresql-16"]
2 | run = "npm run dev"
3 | hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
4 |
5 | [nix]
6 | channel = "stable-24_05"
7 |
8 | [deployment]
9 | deploymentTarget = "cloudrun"
10 | build = ["npm", "run", "build"]
11 | run = ["npm", "run", "start"]
12 |
13 | [[ports]]
14 | localPort = 5000
15 | externalPort = 80
16 |
17 | [workflows]
18 | runButton = "Project"
19 |
20 | [[workflows.workflow]]
21 | name = "Project"
22 | mode = "parallel"
23 | author = "agent"
24 |
25 | [[workflows.workflow.tasks]]
26 | task = "workflow.run"
27 | args = "Start application"
28 |
29 | [[workflows.workflow]]
30 | name = "Start application"
31 | author = "agent"
32 |
33 | [workflows.workflow.metadata]
34 | agentRequireRestartOnSave = false
35 |
36 | [[workflows.workflow.tasks]]
37 | task = "packager.installForAll"
38 |
39 | [[workflows.workflow.tasks]]
40 | task = "shell.exec"
41 | args = "npm run dev"
42 | waitForPort = 5000
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Pippin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/src/lib/websocket.ts:
--------------------------------------------------------------------------------
1 | import { Task } from "@db/schema";
2 |
3 | type WebSocketMessage = {
4 | type: "TASK_CREATED" | "TASK_UPDATED" | "TASK_DELETED";
5 | task?: Task;
6 | taskId?: number;
7 | };
8 |
9 | export class TaskWebSocket {
10 | private ws: WebSocket;
11 | private messageHandlers: ((message: WebSocketMessage) => void)[] = [];
12 |
13 | constructor() {
14 | const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
15 | this.ws = new WebSocket(`${protocol}//${window.location.host}`);
16 |
17 | this.ws.onmessage = (event) => {
18 | const message: WebSocketMessage = JSON.parse(event.data);
19 | this.messageHandlers.forEach((handler) => handler(message));
20 | };
21 |
22 | this.ws.onclose = () => {
23 | setTimeout(() => new TaskWebSocket(), 1000);
24 | };
25 | }
26 |
27 | public onMessage(handler: (message: WebSocketMessage) => void) {
28 | this.messageHandlers.push(handler);
29 | return () => {
30 | this.messageHandlers = this.messageHandlers.filter((h) => h !== handler);
31 | };
32 | }
33 | }
34 |
35 | export const taskWebSocket = new TaskWebSocket();
36 |
--------------------------------------------------------------------------------
/client/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { Check } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ))
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
27 |
28 | export { Checkbox }
29 |
--------------------------------------------------------------------------------
/client/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SliderPrimitive from "@radix-ui/react-slider"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
19 |
20 |
21 |
22 |
23 | ))
24 | Slider.displayName = SliderPrimitive.Root.displayName
25 |
26 | export { Slider }
27 |
--------------------------------------------------------------------------------
/client/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitives from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ))
25 | Switch.displayName = SwitchPrimitives.Root.displayName
26 |
27 | export { Switch }
28 |
--------------------------------------------------------------------------------
/client/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/client/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/client/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const HoverCard = HoverCardPrimitive.Root
7 |
8 | const HoverCardTrigger = HoverCardPrimitive.Trigger
9 |
10 | const HoverCardContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
24 | ))
25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
26 |
27 | export { HoverCard, HoverCardTrigger, HoverCardContent }
28 |
--------------------------------------------------------------------------------
/client/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ))
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
28 |
29 | export { Popover, PopoverTrigger, PopoverContent }
30 |
--------------------------------------------------------------------------------
/client/src/lib/api.ts:
--------------------------------------------------------------------------------
1 | import { Task, ApiTest } from "@db/schema";
2 |
3 | const API_BASE = "/api";
4 |
5 | export async function fetchTasks(): Promise {
6 | const response = await fetch(`${API_BASE}/tasks`);
7 | return response.json();
8 | }
9 |
10 | export async function createTask(task: Omit): Promise {
11 | const response = await fetch(`${API_BASE}/tasks`, {
12 | method: "POST",
13 | headers: { "Content-Type": "application/json" },
14 | body: JSON.stringify(task),
15 | });
16 | return response.json();
17 | }
18 |
19 | export async function updateTask(id: number, task: Partial): Promise {
20 | const response = await fetch(`${API_BASE}/tasks/${id}`, {
21 | method: "PUT",
22 | headers: { "Content-Type": "application/json" },
23 | body: JSON.stringify(task),
24 | });
25 | return response.json();
26 | }
27 |
28 | export async function deleteTask(id: number): Promise {
29 | await fetch(`${API_BASE}/tasks/${id}`, { method: "DELETE" });
30 | }
31 |
32 | export async function createApiTest(test: Omit): Promise {
33 | const response = await fetch(`${API_BASE}/tests`, {
34 | method: "POST",
35 | headers: { "Content-Type": "application/json" },
36 | body: JSON.stringify(test),
37 | });
38 | return response.json();
39 | }
40 |
41 | export async function fetchApiTests(): Promise {
42 | const response = await fetch(`${API_BASE}/tests`);
43 | return response.json();
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = AvatarPrimitive.Root.displayName
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47 |
48 | export { Avatar, AvatarImage, AvatarFallback }
49 |
--------------------------------------------------------------------------------
/client/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-10 px-3",
18 | sm: "h-9 px-2.5",
19 | lg: "h-11 px-5",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName
42 |
43 | export { Toggle, toggleVariants }
44 |
--------------------------------------------------------------------------------
/client/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
3 | import { Circle } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return (
12 |
17 | )
18 | })
19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
20 |
21 | const RadioGroupItem = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => {
25 | return (
26 |
34 |
35 |
36 |
37 |
38 | )
39 | })
40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
41 |
42 | export { RadioGroup, RadioGroupItem }
43 |
--------------------------------------------------------------------------------
/client/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | ))
22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23 |
24 | const ScrollBar = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, orientation = "vertical", ...props }, ref) => (
28 |
41 |
42 |
43 | ))
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45 |
46 | export { ScrollArea, ScrollBar }
47 |
--------------------------------------------------------------------------------
/client/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/client/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import { GripVertical } from "lucide-react"
2 | import * as ResizablePrimitive from "react-resizable-panels"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ResizablePanelGroup = ({
7 | className,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
17 | )
18 |
19 | const ResizablePanel = ResizablePrimitive.Panel
20 |
21 | const ResizableHandle = ({
22 | withHandle,
23 | className,
24 | ...props
25 | }: React.ComponentProps & {
26 | withHandle?: boolean
27 | }) => (
28 | div]:rotate-90",
31 | className
32 | )}
33 | {...props}
34 | >
35 | {withHandle && (
36 |
37 |
38 |
39 | )}
40 |
41 | )
42 |
43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
44 |
--------------------------------------------------------------------------------
/client/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
3 | import { type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | })
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 |
26 | {children}
27 |
28 |
29 | ))
30 |
31 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
32 |
33 | const ToggleGroupItem = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef &
36 | VariantProps
37 | >(({ className, children, variant, size, ...props }, ref) => {
38 | const context = React.useContext(ToggleGroupContext)
39 |
40 | return (
41 |
52 | {children}
53 |
54 | )
55 | })
56 |
57 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
58 |
59 | export { ToggleGroup, ToggleGroupItem }
60 |
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | import express, { type Request, Response, NextFunction } from "express";
2 | import { registerRoutes } from "./routes";
3 | import { setupVite, serveStatic, log } from "./vite";
4 |
5 | const app = express();
6 | app.use(express.json());
7 | app.use(express.urlencoded({ extended: false }));
8 |
9 | app.use((req, res, next) => {
10 | const start = Date.now();
11 | const path = req.path;
12 | let capturedJsonResponse: Record | undefined = undefined;
13 |
14 | const originalResJson = res.json;
15 | res.json = function (bodyJson, ...args) {
16 | capturedJsonResponse = bodyJson;
17 | return originalResJson.apply(res, [bodyJson, ...args]);
18 | };
19 |
20 | res.on("finish", () => {
21 | const duration = Date.now() - start;
22 | if (path.startsWith("/api")) {
23 | let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
24 | if (capturedJsonResponse) {
25 | logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
26 | }
27 |
28 | if (logLine.length > 80) {
29 | logLine = logLine.slice(0, 79) + "…";
30 | }
31 |
32 | log(logLine);
33 | }
34 | });
35 |
36 | next();
37 | });
38 |
39 | (async () => {
40 | const server = registerRoutes(app);
41 |
42 | app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
43 | const status = err.status || err.statusCode || 500;
44 | const message = err.message || "Internal Server Error";
45 |
46 | res.status(status).json({ message });
47 | throw err;
48 | });
49 |
50 | // importantly only setup vite in development and after
51 | // setting up all the other routes so the catch-all route
52 | // doesn't interfere with the other routes
53 | if (app.get("env") === "development") {
54 | await setupVite(app, server);
55 | } else {
56 | serveStatic(app);
57 | }
58 |
59 | // ALWAYS serve the app on port 5000
60 | // this serves both the API and the client
61 | const PORT = 5000;
62 | server.listen(PORT, "0.0.0.0", () => {
63 | log(`serving on port ${PORT}`);
64 | });
65 | })();
66 |
--------------------------------------------------------------------------------
/client/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/client/src/pages/test-dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { fetchApiTests } from "@/lib/api";
3 | import { RequestBuilder } from "@/components/api-test/request-builder";
4 | import { ResponseViewer } from "@/components/api-test/response-viewer";
5 | import { Card } from "@/components/ui/card";
6 | import { Link } from "wouter";
7 | import { Button } from "@/components/ui/button";
8 | import { LayoutDashboard, Beaker } from "lucide-react";
9 |
10 | export default function TestDashboard() {
11 | const { data: tests = [] } = useQuery({
12 | queryKey: ["/api/tests"],
13 | queryFn: fetchApiTests,
14 | });
15 |
16 | return (
17 |
18 |
38 |
39 |
40 |
41 |
42 | Request Builder
43 |
44 |
45 |
46 | Response Viewer
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/client/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/client/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 |
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
55 |
56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
57 |
--------------------------------------------------------------------------------
/client/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { pgTable, text, serial, timestamp, integer } from "drizzle-orm/pg-core";
2 | import { createInsertSchema, createSelectSchema } from "drizzle-zod";
3 | import { relations } from "drizzle-orm";
4 |
5 | export const tasks = pgTable("tasks", {
6 | id: serial("id").primaryKey(),
7 | title: text("title").notNull(),
8 | description: text("description"),
9 | status: text("status").notNull().default("pending"),
10 | assignedTo: text("assigned_to"),
11 | priority: text("priority").notNull().default("medium"),
12 | createdAt: timestamp("created_at").defaultNow().notNull(),
13 | updatedAt: timestamp("updated_at").defaultNow().notNull(),
14 | });
15 |
16 | export const apiTests = pgTable("api_tests", {
17 | id: serial("id").primaryKey(),
18 | endpoint: text("endpoint").notNull(),
19 | method: text("method").notNull(),
20 | requestBody: text("request_body"),
21 | responseStatus: integer("response_status"),
22 | responseBody: text("response_body"),
23 | createdAt: timestamp("created_at").defaultNow().notNull(),
24 | });
25 |
26 | export const metrics = pgTable("metrics", {
27 | id: serial("id").primaryKey(),
28 | endpoint: text("endpoint").notNull(),
29 | method: text("method").notNull(),
30 | responseTime: integer("response_time").notNull(), // in milliseconds
31 | responseStatus: integer("response_status").notNull(),
32 | timestamp: timestamp("timestamp").defaultNow().notNull(),
33 | });
34 |
35 | export const taskRelations = relations(tasks, ({ many }) => ({
36 | apiTests: many(apiTests),
37 | }));
38 |
39 | export const insertTaskSchema = createInsertSchema(tasks);
40 | export const selectTaskSchema = createSelectSchema(tasks);
41 | export const insertApiTestSchema = createInsertSchema(apiTests);
42 | export const selectApiTestSchema = createSelectSchema(apiTests);
43 | export const insertMetricSchema = createInsertSchema(metrics);
44 | export const selectMetricSchema = createSelectSchema(metrics);
45 |
46 | export type Task = typeof tasks.$inferSelect;
47 | export type NewTask = typeof tasks.$inferInsert;
48 | export type ApiTest = typeof apiTests.$inferSelect;
49 | export type NewApiTest = typeof apiTests.$inferInsert;
50 | export type Metric = typeof metrics.$inferSelect;
51 | export type NewMetric = typeof metrics.$inferInsert;
--------------------------------------------------------------------------------
/client/src/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { OTPInput, OTPInputContext } from "input-otp"
3 | import { Dot } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const InputOTP = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, containerClassName, ...props }, ref) => (
11 |
20 | ))
21 | InputOTP.displayName = "InputOTP"
22 |
23 | const InputOTPGroup = React.forwardRef<
24 | React.ElementRef<"div">,
25 | React.ComponentPropsWithoutRef<"div">
26 | >(({ className, ...props }, ref) => (
27 |
28 | ))
29 | InputOTPGroup.displayName = "InputOTPGroup"
30 |
31 | const InputOTPSlot = React.forwardRef<
32 | React.ElementRef<"div">,
33 | React.ComponentPropsWithoutRef<"div"> & { index: number }
34 | >(({ index, className, ...props }, ref) => {
35 | const inputOTPContext = React.useContext(OTPInputContext)
36 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
37 |
38 | return (
39 |
48 | {char}
49 | {hasFakeCaret && (
50 |
53 | )}
54 |
55 | )
56 | })
57 | InputOTPSlot.displayName = "InputOTPSlot"
58 |
59 | const InputOTPSeparator = React.forwardRef<
60 | React.ElementRef<"div">,
61 | React.ComponentPropsWithoutRef<"div">
62 | >(({ ...props }, ref) => (
63 |
64 |
65 |
66 | ))
67 | InputOTPSeparator.displayName = "InputOTPSeparator"
68 |
69 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
70 |
--------------------------------------------------------------------------------
/server/vite.ts:
--------------------------------------------------------------------------------
1 | import express, { type Express } from "express";
2 | import fs from "fs";
3 | import path, { dirname } from "path";
4 | import { fileURLToPath } from "url";
5 | import { createServer as createViteServer, createLogger } from "vite";
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = dirname(__filename);
8 | import { type Server } from "http";
9 | import viteConfig from "../vite.config";
10 | import { nanoid } from "nanoid";
11 |
12 | const viteLogger = createLogger();
13 |
14 | export function log(message: string, source = "express") {
15 | const formattedTime = new Date().toLocaleTimeString("en-US", {
16 | hour: "numeric",
17 | minute: "2-digit",
18 | second: "2-digit",
19 | hour12: true,
20 | });
21 |
22 | console.log(`${formattedTime} [${source}] ${message}`);
23 | }
24 |
25 | export async function setupVite(app: Express, server: Server) {
26 | const vite = await createViteServer({
27 | ...viteConfig,
28 | configFile: false,
29 | customLogger: {
30 | ...viteLogger,
31 | error: (msg, options) => {
32 | viteLogger.error(msg, options);
33 | process.exit(1);
34 | },
35 | },
36 | server: {
37 | middlewareMode: true,
38 | hmr: { server },
39 | },
40 | appType: "custom",
41 | });
42 |
43 | app.use(vite.middlewares);
44 | app.use("*", async (req, res, next) => {
45 | const url = req.originalUrl;
46 |
47 | try {
48 | const clientTemplate = path.resolve(
49 | __dirname,
50 | "..",
51 | "client",
52 | "index.html",
53 | );
54 |
55 | // always reload the index.html file from disk incase it changes
56 | let template = await fs.promises.readFile(clientTemplate, "utf-8");
57 | template = template.replace(`src="/src/main.tsx"`, `src="/src/main.tsx?v=${nanoid()}"`)
58 | const page = await vite.transformIndexHtml(url, template);
59 | res.status(200).set({ "Content-Type": "text/html" }).end(page);
60 | } catch (e) {
61 | vite.ssrFixStacktrace(e as Error);
62 | next(e);
63 | }
64 | });
65 | }
66 |
67 | export function serveStatic(app: Express) {
68 | const distPath = path.resolve(__dirname, "public");
69 |
70 | if (!fs.existsSync(distPath)) {
71 | throw new Error(
72 | `Could not find the build directory: ${distPath}, make sure to build the client first`,
73 | );
74 | }
75 |
76 | app.use(express.static(distPath));
77 |
78 | // fall through to index.html if the file doesn't exist
79 | app.use("*", (_req, res) => {
80 | res.sendFile(path.resolve(distPath, "index.html"));
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/client/src/pages/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { fetchTasks } from "@/lib/api";
3 | import { TaskList } from "@/components/tasks/task-list";
4 | import { TaskForm } from "@/components/tasks/task-form";
5 | import { StatusChart } from "@/components/tasks/status-chart";
6 | import { Card } from "@/components/ui/card";
7 | import { Link } from "wouter";
8 | import { Button } from "@/components/ui/button";
9 | import { BeakerIcon, LayoutDashboard, BarChart2 } from "lucide-react";
10 |
11 | export default function Dashboard() {
12 | const { data: tasks = [] } = useQuery({
13 | queryKey: ["/api/tasks"],
14 | queryFn: fetchTasks,
15 | });
16 |
17 | return (
18 |
19 |
45 |
46 |
47 |
48 |
49 |
50 | Tasks
51 |
52 |
53 |
54 |
55 |
56 | Add Task
57 |
58 |
59 |
60 | Status Distribution
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
--------------------------------------------------------------------------------
/client/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight } from "lucide-react"
3 | import { DayPicker } from "react-day-picker"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { buttonVariants } from "@/components/ui/button"
7 |
8 | export type CalendarProps = React.ComponentProps
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: CalendarProps) {
16 | return (
17 | ,
56 | IconRight: ({ ...props }) => ,
57 | }}
58 | {...props}
59 | />
60 | )
61 | }
62 | Calendar.displayName = "Calendar"
63 |
64 | export { Calendar }
65 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
5 | content: ["./client/index.html", "./client/src/**/*.{js,jsx,ts,tsx}"],
6 | theme: {
7 | extend: {
8 | borderRadius: {
9 | lg: "var(--radius)",
10 | md: "calc(var(--radius) - 2px)",
11 | sm: "calc(var(--radius) - 4px)",
12 | },
13 | colors: {
14 | background: "hsl(var(--background))",
15 | foreground: "hsl(var(--foreground))",
16 | card: {
17 | DEFAULT: "hsl(var(--card))",
18 | foreground: "hsl(var(--card-foreground))",
19 | },
20 | popover: {
21 | DEFAULT: "hsl(var(--popover))",
22 | foreground: "hsl(var(--popover-foreground))",
23 | },
24 | primary: {
25 | DEFAULT: "hsl(var(--primary))",
26 | foreground: "hsl(var(--primary-foreground))",
27 | },
28 | secondary: {
29 | DEFAULT: "hsl(var(--secondary))",
30 | foreground: "hsl(var(--secondary-foreground))",
31 | },
32 | muted: {
33 | DEFAULT: "hsl(var(--muted))",
34 | foreground: "hsl(var(--muted-foreground))",
35 | },
36 | accent: {
37 | DEFAULT: "hsl(var(--accent))",
38 | foreground: "hsl(var(--accent-foreground))",
39 | },
40 | destructive: {
41 | DEFAULT: "hsl(var(--destructive))",
42 | foreground: "hsl(var(--destructive-foreground))",
43 | },
44 | border: "hsl(var(--border))",
45 | input: "hsl(var(--input))",
46 | ring: "hsl(var(--ring))",
47 | chart: {
48 | "1": "hsl(var(--chart-1))",
49 | "2": "hsl(var(--chart-2))",
50 | "3": "hsl(var(--chart-3))",
51 | "4": "hsl(var(--chart-4))",
52 | "5": "hsl(var(--chart-5))",
53 | },
54 | sidebar: {
55 | DEFAULT: "hsl(var(--sidebar-background))",
56 | foreground: "hsl(var(--sidebar-foreground))",
57 | primary: "hsl(var(--sidebar-primary))",
58 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
59 | accent: "hsl(var(--sidebar-accent))",
60 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
61 | border: "hsl(var(--sidebar-border))",
62 | ring: "hsl(var(--sidebar-ring))",
63 | },
64 | },
65 | keyframes: {
66 | "accordion-down": {
67 | from: {
68 | height: "0",
69 | },
70 | to: {
71 | height: "var(--radix-accordion-content-height)",
72 | },
73 | },
74 | "accordion-up": {
75 | from: {
76 | height: "var(--radix-accordion-content-height)",
77 | },
78 | to: {
79 | height: "0",
80 | },
81 | },
82 | },
83 | animation: {
84 | "accordion-down": "accordion-down 0.2s ease-out",
85 | "accordion-up": "accordion-up 0.2s ease-out",
86 | },
87 | },
88 | },
89 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
90 | } satisfies Config;
91 |
--------------------------------------------------------------------------------
/client/src/components/tasks/status-chart.tsx:
--------------------------------------------------------------------------------
1 | import { Task } from "@db/schema";
2 | import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from "recharts";
3 | import { useEffect } from "react";
4 | import { taskWebSocket } from "@/lib/websocket";
5 | import { useQueryClient } from "@tanstack/react-query";
6 |
7 | interface StatusChartProps {
8 | tasks: Task[];
9 | }
10 |
11 | const COLORS = {
12 | pending: "#EAB308",
13 | "in-progress": "#3B82F6",
14 | completed: "#22C55E",
15 | cancelled: "#EF4444",
16 | };
17 |
18 | export function StatusChart({ tasks }: StatusChartProps) {
19 | const queryClient = useQueryClient();
20 |
21 | // Subscribe to WebSocket updates
22 | useEffect(() => {
23 | const unsubscribe = taskWebSocket.onMessage(() => {
24 | // Invalidate tasks query to trigger a refresh
25 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
26 | });
27 |
28 | return () => unsubscribe();
29 | }, [queryClient]);
30 |
31 | const statusCounts = tasks.reduce(
32 | (acc, task) => {
33 | acc[task.status] = (acc[task.status] || 0) + 1;
34 | return acc;
35 | },
36 | {} as Record
37 | );
38 |
39 | const data = Object.entries(statusCounts).map(([name, value]) => ({
40 | name,
41 | value,
42 | }));
43 |
44 | return (
45 |
46 |
47 |
48 | {
64 | const RADIAN = Math.PI / 180;
65 | const radius = 25 + innerRadius + (outerRadius - innerRadius);
66 | const x = cx + radius * Math.cos(-midAngle * RADIAN);
67 | const y = cy + radius * Math.sin(-midAngle * RADIAN);
68 |
69 | return (
70 | cx ? "start" : "end"}
75 | dominantBaseline="central"
76 | >
77 | {`${value}`}
78 |
79 | );
80 | }}
81 | >
82 | {data.map((entry, index) => (
83 | |
87 | ))}
88 |
89 |
97 |
98 |
99 | );
100 | }
--------------------------------------------------------------------------------
/client/src/components/api-test/response-viewer.tsx:
--------------------------------------------------------------------------------
1 | import { ApiTest } from "@db/schema";
2 | import {
3 | Accordion,
4 | AccordionContent,
5 | AccordionItem,
6 | AccordionTrigger,
7 | } from "@/components/ui/accordion";
8 | import { Badge } from "@/components/ui/badge";
9 | import { ScrollArea } from "@/components/ui/scroll-area";
10 | import { format } from "date-fns";
11 |
12 | interface ResponseViewerProps {
13 | tests: ApiTest[];
14 | }
15 |
16 | const statusColors: Record = {
17 | "2": "bg-green-500",
18 | "3": "bg-blue-500",
19 | "4": "bg-yellow-500",
20 | "5": "bg-red-500",
21 | };
22 |
23 | export function ResponseViewer({ tests }: ResponseViewerProps) {
24 | return (
25 |
26 |
27 | {tests.map((test) => (
28 |
29 |
30 |
31 |
40 | {test.responseStatus || 500}
41 |
42 |
43 | {test.method} {test.endpoint}
44 |
45 |
46 | {format(new Date(test.createdAt), "MMM d, yyyy HH:mm:ss")}
47 |
48 |
49 |
50 |
51 |
52 | {test.requestBody && (
53 |
54 |
Request Body
55 |
56 |
57 | {JSON.stringify(
58 | JSON.parse(test.requestBody),
59 | null,
60 | 2
61 | )}
62 |
63 |
64 |
65 | )}
66 | {test.responseBody && (
67 |
68 |
Response Body
69 |
70 |
71 | {JSON.stringify(
72 | JSON.parse(test.responseBody),
73 | null,
74 | 2
75 | )}
76 |
77 |
78 |
79 | )}
80 |
81 |
82 |
83 | ))}
84 |
85 |
86 | );
87 | }
--------------------------------------------------------------------------------
/client/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/client/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { ButtonProps, buttonVariants } from "@/components/ui/button"
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8 |
14 | )
15 | Pagination.displayName = "Pagination"
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<"ul">
20 | >(({ className, ...props }, ref) => (
21 |
26 | ))
27 | PaginationContent.displayName = "PaginationContent"
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<"li">
32 | >(({ className, ...props }, ref) => (
33 |
34 | ))
35 | PaginationItem.displayName = "PaginationItem"
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean
39 | } & Pick &
40 | React.ComponentProps<"a">
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = "icon",
46 | ...props
47 | }: PaginationLinkProps) => (
48 |
59 | )
60 | PaginationLink.displayName = "PaginationLink"
61 |
62 | const PaginationPrevious = ({
63 | className,
64 | ...props
65 | }: React.ComponentProps) => (
66 |
72 |
73 | Previous
74 |
75 | )
76 | PaginationPrevious.displayName = "PaginationPrevious"
77 |
78 | const PaginationNext = ({
79 | className,
80 | ...props
81 | }: React.ComponentProps) => (
82 |
88 | Next
89 |
90 |
91 | )
92 | PaginationNext.displayName = "PaginationNext"
93 |
94 | const PaginationEllipsis = ({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"span">) => (
98 |
103 |
104 | More pages
105 |
106 | )
107 | PaginationEllipsis.displayName = "PaginationEllipsis"
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | }
118 |
--------------------------------------------------------------------------------
/client/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/client/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Drawer as DrawerPrimitive } from "vaul"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Drawer = ({
7 | shouldScaleBackground = true,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
14 | )
15 | Drawer.displayName = "Drawer"
16 |
17 | const DrawerTrigger = DrawerPrimitive.Trigger
18 |
19 | const DrawerPortal = DrawerPrimitive.Portal
20 |
21 | const DrawerClose = DrawerPrimitive.Close
22 |
23 | const DrawerOverlay = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
34 |
35 | const DrawerContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, ...props }, ref) => (
39 |
40 |
41 |
49 |
50 | {children}
51 |
52 |
53 | ))
54 | DrawerContent.displayName = "DrawerContent"
55 |
56 | const DrawerHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
64 | )
65 | DrawerHeader.displayName = "DrawerHeader"
66 |
67 | const DrawerFooter = ({
68 | className,
69 | ...props
70 | }: React.HTMLAttributes) => (
71 |
75 | )
76 | DrawerFooter.displayName = "DrawerFooter"
77 |
78 | const DrawerTitle = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef
81 | >(({ className, ...props }, ref) => (
82 |
90 | ))
91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
92 |
93 | const DrawerDescription = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
102 | ))
103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
104 |
105 | export {
106 | Drawer,
107 | DrawerPortal,
108 | DrawerOverlay,
109 | DrawerTrigger,
110 | DrawerClose,
111 | DrawerContent,
112 | DrawerHeader,
113 | DrawerFooter,
114 | DrawerTitle,
115 | DrawerDescription,
116 | }
117 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rest-express",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "tsx server/index.ts",
8 | "build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
9 | "start": "NODE_ENV=production node dist/index.js",
10 | "check": "tsc",
11 | "db:push": "drizzle-kit push"
12 | },
13 | "dependencies": {
14 | "@hookform/resolvers": "^3.9.1",
15 | "@jridgewell/trace-mapping": "^0.3.25",
16 | "@radix-ui/react-accordion": "^1.2.1",
17 | "@radix-ui/react-alert-dialog": "^1.1.2",
18 | "@radix-ui/react-aspect-ratio": "^1.1.0",
19 | "@radix-ui/react-avatar": "^1.1.1",
20 | "@radix-ui/react-checkbox": "^1.1.2",
21 | "@radix-ui/react-collapsible": "^1.1.1",
22 | "@radix-ui/react-context-menu": "^2.2.2",
23 | "@radix-ui/react-dialog": "^1.1.2",
24 | "@radix-ui/react-dropdown-menu": "^2.1.2",
25 | "@radix-ui/react-hover-card": "^1.1.2",
26 | "@radix-ui/react-label": "^2.1.0",
27 | "@radix-ui/react-menubar": "^1.1.2",
28 | "@radix-ui/react-navigation-menu": "^1.2.1",
29 | "@radix-ui/react-popover": "^1.1.2",
30 | "@radix-ui/react-progress": "^1.1.0",
31 | "@radix-ui/react-radio-group": "^1.2.1",
32 | "@radix-ui/react-scroll-area": "^1.2.0",
33 | "@radix-ui/react-select": "^2.1.2",
34 | "@radix-ui/react-separator": "^1.1.0",
35 | "@radix-ui/react-slider": "^1.2.1",
36 | "@radix-ui/react-slot": "^1.1.0",
37 | "@radix-ui/react-switch": "^1.1.1",
38 | "@radix-ui/react-tabs": "^1.1.1",
39 | "@radix-ui/react-toast": "^1.2.2",
40 | "@radix-ui/react-toggle": "^1.1.0",
41 | "@radix-ui/react-toggle-group": "^1.1.0",
42 | "@radix-ui/react-tooltip": "^1.1.3",
43 | "@replit/vite-plugin-shadcn-theme-json": "^0.0.4",
44 | "@tanstack/react-query": "^5.60.5",
45 | "class-variance-authority": "^0.7.0",
46 | "clsx": "^2.1.1",
47 | "cmdk": "^1.0.0",
48 | "date-fns": "^3.6.0",
49 | "drizzle-orm": "^0.38.2",
50 | "drizzle-zod": "^0.6.0",
51 | "embla-carousel-react": "^8.3.0",
52 | "express": "^4.21.2",
53 | "express-session": "^1.18.1",
54 | "framer-motion": "^11.13.1",
55 | "input-otp": "^1.2.4",
56 | "lucide-react": "^0.453.0",
57 | "memorystore": "^1.6.7",
58 | "passport": "^0.7.0",
59 | "passport-local": "^1.0.0",
60 | "react": "^18.3.1",
61 | "react-day-picker": "^8.10.1",
62 | "react-dom": "^18.3.1",
63 | "react-hook-form": "^7.53.1",
64 | "react-icons": "^5.4.0",
65 | "react-resizable-panels": "^2.1.4",
66 | "recharts": "^2.13.0",
67 | "tailwind-merge": "^2.5.4",
68 | "tailwindcss-animate": "^1.0.7",
69 | "vaul": "^1.1.0",
70 | "wouter": "^3.3.5",
71 | "ws": "^8.18.0",
72 | "zod": "^3.23.8"
73 | },
74 | "devDependencies": {
75 | "@replit/vite-plugin-runtime-error-modal": "^0.0.3",
76 | "@tailwindcss/typography": "^0.5.15",
77 | "@types/express": "4.17.21",
78 | "@types/express-session": "^1.18.0",
79 | "@types/node": "20.16.11",
80 | "@types/passport": "^1.0.16",
81 | "@types/passport-local": "^1.0.38",
82 | "@types/react": "^18.3.11",
83 | "@types/react-dom": "^18.3.1",
84 | "@types/ws": "^8.5.13",
85 | "@vitejs/plugin-react": "^4.3.2",
86 | "autoprefixer": "^10.4.20",
87 | "drizzle-kit": "^0.27.1",
88 | "esbuild": "^0.24.0",
89 | "postcss": "^8.4.47",
90 | "tailwindcss": "^3.4.14",
91 | "tsx": "^4.19.1",
92 | "typescript": "5.6.3",
93 | "vite": "^5.4.9"
94 | },
95 | "optionalDependencies": {
96 | "bufferutil": "^4.0.8"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/client/src/components/tasks/task-list.tsx:
--------------------------------------------------------------------------------
1 | import { Task } from "@db/schema";
2 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
3 | import { Badge } from "@/components/ui/badge";
4 | import { Button } from "@/components/ui/button";
5 | import { Trash2, Edit2 } from "lucide-react";
6 | import { useMutation, useQueryClient } from "@tanstack/react-query";
7 | import { deleteTask, updateTask } from "@/lib/api";
8 | import { useToast } from "@/hooks/use-toast";
9 | import {
10 | Dialog,
11 | DialogContent,
12 | DialogHeader,
13 | DialogTitle,
14 | DialogTrigger,
15 | } from "@/components/ui/dialog";
16 | import { TaskForm } from "./task-form";
17 |
18 | interface TaskListProps {
19 | tasks: Task[];
20 | }
21 |
22 | const statusColors = {
23 | pending: "bg-yellow-500",
24 | "in-progress": "bg-blue-500",
25 | completed: "bg-green-500",
26 | cancelled: "bg-red-500",
27 | };
28 |
29 | const priorityColors = {
30 | low: "bg-gray-500",
31 | medium: "bg-orange-500",
32 | high: "bg-red-500",
33 | };
34 |
35 | export function TaskList({ tasks }: TaskListProps) {
36 | const queryClient = useQueryClient();
37 | const { toast } = useToast();
38 |
39 | const deleteMutation = useMutation({
40 | mutationFn: deleteTask,
41 | onSuccess: () => {
42 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
43 | toast({
44 | title: "Task deleted",
45 | description: "The task has been successfully deleted.",
46 | });
47 | },
48 | });
49 |
50 | const updateMutation = useMutation({
51 | mutationFn: ({ id, task }: { id: number; task: Partial }) =>
52 | updateTask(id, task),
53 | onSuccess: () => {
54 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
55 | toast({
56 | title: "Task updated",
57 | description: "The task has been successfully updated.",
58 | });
59 | },
60 | });
61 |
62 | return (
63 |
64 |
65 |
66 | Title
67 | Status
68 | Priority
69 | Assigned To
70 | Actions
71 |
72 |
73 |
74 | {tasks.map((task) => (
75 |
76 | {task.title}
77 |
78 |
83 | {task.status}
84 |
85 |
86 |
87 |
92 | {task.priority}
93 |
94 |
95 | {task.assignedTo || "Unassigned"}
96 |
97 |
110 |
117 |
118 |
119 | ))}
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/server/routes.ts:
--------------------------------------------------------------------------------
1 | import type { Express } from "express";
2 | import { createServer, type Server } from "http";
3 | import { WebSocketServer } from "ws";
4 | import { db } from "@db";
5 | import { tasks, apiTests, metrics } from "@db/schema";
6 | import { eq } from "drizzle-orm";
7 |
8 | async function logMetrics(endpoint: string, method: string, responseTime: number, status: number) {
9 | await db.insert(metrics).values({
10 | endpoint,
11 | method,
12 | responseTime,
13 | responseStatus: status,
14 | });
15 | }
16 |
17 | export function registerRoutes(app: Express): Server {
18 | const httpServer = createServer(app);
19 | const wss = new WebSocketServer({
20 | server: httpServer,
21 | verifyClient: ({ req }) => {
22 | // Ignore Vite HMR WebSocket connections
23 | return !req.headers['sec-websocket-protocol']?.includes('vite-hmr');
24 | }
25 | });
26 |
27 | // WebSocket connection handling
28 | wss.on("connection", (ws) => {
29 | ws.on("message", (message) => {
30 | // Broadcast updates to all clients
31 | wss.clients.forEach((client) => {
32 | if (client !== ws) {
33 | client.send(message.toString());
34 | }
35 | });
36 | });
37 | });
38 |
39 | // Performance monitoring middleware
40 | app.use((req, res, next) => {
41 | const start = Date.now();
42 | const endpoint = req.path;
43 | const method = req.method;
44 |
45 | // Capture the original end function
46 | const originalEnd = res.end;
47 |
48 | // Override the end function to calculate response time
49 | res.end = function (...args) {
50 | const responseTime = Date.now() - start;
51 | const status = res.statusCode;
52 |
53 | // Log metrics only for API endpoints
54 | if (endpoint.startsWith('/api')) {
55 | logMetrics(endpoint, method, responseTime, status).catch(console.error);
56 | }
57 |
58 | // Call the original end function
59 | return originalEnd.apply(res, args);
60 | };
61 |
62 | next();
63 | });
64 |
65 | // Task Management Routes
66 | app.get("/api/tasks", async (_req, res) => {
67 | const allTasks = await db.query.tasks.findMany({
68 | orderBy: (tasks, { desc }) => [desc(tasks.createdAt)],
69 | });
70 | res.json(allTasks);
71 | });
72 |
73 | app.post("/api/tasks", async (req, res) => {
74 | const newTask = await db.insert(tasks).values(req.body).returning();
75 | wss.clients.forEach((client) => {
76 | client.send(JSON.stringify({ type: "TASK_CREATED", task: newTask[0] }));
77 | });
78 | res.json(newTask[0]);
79 | });
80 |
81 | app.put("/api/tasks/:id", async (req, res) => {
82 | const taskId = parseInt(req.params.id);
83 | const updatedTask = await db
84 | .update(tasks)
85 | .set(req.body)
86 | .where(eq(tasks.id, taskId))
87 | .returning();
88 | wss.clients.forEach((client) => {
89 | client.send(JSON.stringify({ type: "TASK_UPDATED", task: updatedTask[0] }));
90 | });
91 | res.json(updatedTask[0]);
92 | });
93 |
94 | app.delete("/api/tasks/:id", async (req, res) => {
95 | const taskId = parseInt(req.params.id);
96 | await db.delete(tasks).where(eq(tasks.id, taskId));
97 | wss.clients.forEach((client) => {
98 | client.send(JSON.stringify({ type: "TASK_DELETED", taskId }));
99 | });
100 | res.json({ success: true });
101 | });
102 |
103 | // API Test Routes
104 | app.get("/api/tests", async (_req, res) => {
105 | const allTests = await db.query.apiTests.findMany({
106 | orderBy: (apiTests, { desc }) => [desc(apiTests.createdAt)],
107 | });
108 | res.json(allTests);
109 | });
110 |
111 | app.post("/api/tests", async (req, res) => {
112 | const newTest = await db.insert(apiTests).values(req.body).returning();
113 | res.json(newTest[0]);
114 | });
115 |
116 | // Metrics Routes
117 | app.get("/api/metrics", async (_req, res) => {
118 | const allMetrics = await db.query.metrics.findMany({
119 | orderBy: (metrics, { desc }) => [desc(metrics.timestamp)],
120 | limit: 100, // Limit to last 100 records for performance
121 | });
122 | res.json(allMetrics);
123 | });
124 |
125 | return httpServer;
126 | }
--------------------------------------------------------------------------------
/client/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | *This tool was entirely built, tested, and open-sourced on Github using a combination of OpenAI's Operator using Replit Agent ([video](https://x.com/yoheinakajima/status/1882707264936845329)).*
2 |
3 | # Pippin Tasks
4 |
5 | A comprehensive API-based task management system designed for AI agents and teams, featuring advanced real-time monitoring, robust performance metrics tracking, and dynamic task tracking functionalities.
6 |
7 | ## Features
8 |
9 | - 📋 **Task Management**
10 | - Create, update, and delete tasks
11 | - Real-time status updates via WebSocket
12 | - Priority and assignment tracking
13 | - Task status visualization
14 |
15 | - 🧪 **API Testing Dashboard**
16 | - Interactive API request builder
17 | - Response viewer with syntax highlighting
18 | - Historical test results tracking
19 | - Support for all HTTP methods
20 |
21 | - 📊 **Performance Metrics**
22 | - Real-time API response time tracking
23 | - Error rate monitoring
24 | - Endpoint performance visualization
25 | - Request volume analytics
26 |
27 | ## Tech Stack
28 |
29 | - **Frontend**
30 | - React.js with TypeScript
31 | - TanStack Query for data fetching
32 | - Recharts for data visualization
33 | - Tailwind CSS with shadcn/ui components
34 | - WebSocket for real-time updates
35 |
36 | - **Backend**
37 | - Express.js
38 | - PostgreSQL database
39 | - Drizzle ORM
40 | - WebSocket server
41 | - Performance metrics middleware
42 |
43 | ## Getting Started
44 |
45 | 1. Clone the repository:
46 | ```bash
47 | git clone https://github.com/pippinlovesyou/pippin-tasks.git
48 | cd pippin-tasks
49 | ```
50 |
51 | 2. Install dependencies:
52 | ```bash
53 | npm install
54 | ```
55 |
56 | 3. Set up the database:
57 | ```bash
58 | # Create a PostgreSQL database and set the DATABASE_URL environment variable
59 | npm run db:push
60 | ```
61 |
62 | 4. Start the development server:
63 | ```bash
64 | npm run dev
65 | ```
66 |
67 | ## API Documentation
68 |
69 | ### Tasks
70 |
71 | ```typescript
72 | // Task schema
73 | interface Task {
74 | id: number;
75 | title: string;
76 | description?: string;
77 | status: 'pending' | 'in-progress' | 'completed' | 'cancelled';
78 | assignedTo?: string;
79 | priority: 'low' | 'medium' | 'high';
80 | createdAt: Date;
81 | updatedAt: Date;
82 | }
83 | ```
84 |
85 | #### Endpoints
86 |
87 | - `GET /api/tasks` - Get all tasks
88 | - `POST /api/tasks` - Create a new task
89 | - `PUT /api/tasks/:id` - Update a task
90 | - `DELETE /api/tasks/:id` - Delete a task
91 |
92 | ### API Tests
93 |
94 | ```typescript
95 | // API Test schema
96 | interface ApiTest {
97 | id: number;
98 | endpoint: string;
99 | method: string;
100 | requestBody?: string;
101 | responseStatus: number;
102 | responseBody?: string;
103 | createdAt: Date;
104 | }
105 | ```
106 |
107 | #### Endpoints
108 |
109 | - `GET /api/tests` - Get all API tests
110 | - `POST /api/tests` - Create a new API test
111 |
112 | ### Metrics
113 |
114 | ```typescript
115 | // Metric schema
116 | interface Metric {
117 | id: number;
118 | endpoint: string;
119 | method: string;
120 | responseTime: number;
121 | responseStatus: number;
122 | timestamp: Date;
123 | }
124 | ```
125 |
126 | #### Endpoints
127 |
128 | - `GET /api/metrics` - Get performance metrics
129 |
130 | ## Real-time Updates
131 |
132 | The application uses WebSocket connections for real-time updates. The WebSocket server broadcasts task changes to all connected clients, ensuring that the UI stays synchronized with the latest data.
133 |
134 | ## Development Guidelines
135 |
136 | - Follow the established code structure and patterns
137 | - Use TypeScript for type safety
138 | - Keep the components modular and reusable
139 | - Add appropriate error handling
140 | - Write clear commit messages
141 | - Update documentation when adding new features
142 |
143 | ## Screenshots
144 |
145 | ### Task Dashboard
146 | 
147 |
148 | ### API Test Dashboard
149 | 
150 |
151 | ### Metrics Dashboard
152 | 
153 |
154 | ## Contributing
155 |
156 | 1. Fork the repository
157 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
158 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
159 | 4. Push to the branch (`git push origin feature/amazing-feature`)
160 | 5. Open a Pull Request
161 |
162 | ## License
163 |
164 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
165 |
166 | ## Acknowledgments
167 |
168 | - Built with ❤️ by the Pippin team
169 | - Special thanks to all contributors
170 |
--------------------------------------------------------------------------------
/client/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import type {
4 | ToastActionElement,
5 | ToastProps,
6 | } from "@/components/ui/toast"
7 |
8 | const TOAST_LIMIT = 1
9 | const TOAST_REMOVE_DELAY = 1000000
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string
13 | title?: React.ReactNode
14 | description?: React.ReactNode
15 | action?: ToastActionElement
16 | }
17 |
18 | const actionTypes = {
19 | ADD_TOAST: "ADD_TOAST",
20 | UPDATE_TOAST: "UPDATE_TOAST",
21 | DISMISS_TOAST: "DISMISS_TOAST",
22 | REMOVE_TOAST: "REMOVE_TOAST",
23 | } as const
24 |
25 | let count = 0
26 |
27 | function genId() {
28 | count = (count + 1) % Number.MAX_SAFE_INTEGER
29 | return count.toString()
30 | }
31 |
32 | type ActionType = typeof actionTypes
33 |
34 | type Action =
35 | | {
36 | type: ActionType["ADD_TOAST"]
37 | toast: ToasterToast
38 | }
39 | | {
40 | type: ActionType["UPDATE_TOAST"]
41 | toast: Partial
42 | }
43 | | {
44 | type: ActionType["DISMISS_TOAST"]
45 | toastId?: ToasterToast["id"]
46 | }
47 | | {
48 | type: ActionType["REMOVE_TOAST"]
49 | toastId?: ToasterToast["id"]
50 | }
51 |
52 | interface State {
53 | toasts: ToasterToast[]
54 | }
55 |
56 | const toastTimeouts = new Map>()
57 |
58 | const addToRemoveQueue = (toastId: string) => {
59 | if (toastTimeouts.has(toastId)) {
60 | return
61 | }
62 |
63 | const timeout = setTimeout(() => {
64 | toastTimeouts.delete(toastId)
65 | dispatch({
66 | type: "REMOVE_TOAST",
67 | toastId: toastId,
68 | })
69 | }, TOAST_REMOVE_DELAY)
70 |
71 | toastTimeouts.set(toastId, timeout)
72 | }
73 |
74 | export const reducer = (state: State, action: Action): State => {
75 | switch (action.type) {
76 | case "ADD_TOAST":
77 | return {
78 | ...state,
79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80 | }
81 |
82 | case "UPDATE_TOAST":
83 | return {
84 | ...state,
85 | toasts: state.toasts.map((t) =>
86 | t.id === action.toast.id ? { ...t, ...action.toast } : t
87 | ),
88 | }
89 |
90 | case "DISMISS_TOAST": {
91 | const { toastId } = action
92 |
93 | // ! Side effects ! - This could be extracted into a dismissToast() action,
94 | // but I'll keep it here for simplicity
95 | if (toastId) {
96 | addToRemoveQueue(toastId)
97 | } else {
98 | state.toasts.forEach((toast) => {
99 | addToRemoveQueue(toast.id)
100 | })
101 | }
102 |
103 | return {
104 | ...state,
105 | toasts: state.toasts.map((t) =>
106 | t.id === toastId || toastId === undefined
107 | ? {
108 | ...t,
109 | open: false,
110 | }
111 | : t
112 | ),
113 | }
114 | }
115 | case "REMOVE_TOAST":
116 | if (action.toastId === undefined) {
117 | return {
118 | ...state,
119 | toasts: [],
120 | }
121 | }
122 | return {
123 | ...state,
124 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
125 | }
126 | }
127 | }
128 |
129 | const listeners: Array<(state: State) => void> = []
130 |
131 | let memoryState: State = { toasts: [] }
132 |
133 | function dispatch(action: Action) {
134 | memoryState = reducer(memoryState, action)
135 | listeners.forEach((listener) => {
136 | listener(memoryState)
137 | })
138 | }
139 |
140 | type Toast = Omit
141 |
142 | function toast({ ...props }: Toast) {
143 | const id = genId()
144 |
145 | const update = (props: ToasterToast) =>
146 | dispatch({
147 | type: "UPDATE_TOAST",
148 | toast: { ...props, id },
149 | })
150 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
151 |
152 | dispatch({
153 | type: "ADD_TOAST",
154 | toast: {
155 | ...props,
156 | id,
157 | open: true,
158 | onOpenChange: (open) => {
159 | if (!open) dismiss()
160 | },
161 | },
162 | })
163 |
164 | return {
165 | id: id,
166 | dismiss,
167 | update,
168 | }
169 | }
170 |
171 | function useToast() {
172 | const [state, setState] = React.useState(memoryState)
173 |
174 | React.useEffect(() => {
175 | listeners.push(setState)
176 | return () => {
177 | const index = listeners.indexOf(setState)
178 | if (index > -1) {
179 | listeners.splice(index, 1)
180 | }
181 | }
182 | }, [state])
183 |
184 | return {
185 | ...state,
186 | toast,
187 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
188 | }
189 | }
190 |
191 | export { useToast, toast }
192 |
--------------------------------------------------------------------------------
/client/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SheetPrimitive from "@radix-ui/react-dialog"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Sheet = SheetPrimitive.Root
9 |
10 | const SheetTrigger = SheetPrimitive.Trigger
11 |
12 | const SheetClose = SheetPrimitive.Close
13 |
14 | const SheetPortal = SheetPrimitive.Portal
15 |
16 | const SheetOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ))
29 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
30 |
31 | const sheetVariants = cva(
32 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
33 | {
34 | variants: {
35 | side: {
36 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
37 | bottom:
38 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
39 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
40 | right:
41 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
42 | },
43 | },
44 | defaultVariants: {
45 | side: "right",
46 | },
47 | }
48 | )
49 |
50 | interface SheetContentProps
51 | extends React.ComponentPropsWithoutRef,
52 | VariantProps {}
53 |
54 | const SheetContent = React.forwardRef<
55 | React.ElementRef,
56 | SheetContentProps
57 | >(({ side = "right", className, children, ...props }, ref) => (
58 |
59 |
60 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | ))
73 | SheetContent.displayName = SheetPrimitive.Content.displayName
74 |
75 | const SheetHeader = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetHeader.displayName = "SheetHeader"
88 |
89 | const SheetFooter = ({
90 | className,
91 | ...props
92 | }: React.HTMLAttributes) => (
93 |
100 | )
101 | SheetFooter.displayName = "SheetFooter"
102 |
103 | const SheetTitle = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | SheetTitle.displayName = SheetPrimitive.Title.displayName
114 |
115 | const SheetDescription = React.forwardRef<
116 | React.ElementRef,
117 | React.ComponentPropsWithoutRef
118 | >(({ className, ...props }, ref) => (
119 |
124 | ))
125 | SheetDescription.displayName = SheetPrimitive.Description.displayName
126 |
127 | export {
128 | Sheet,
129 | SheetPortal,
130 | SheetOverlay,
131 | SheetTrigger,
132 | SheetClose,
133 | SheetContent,
134 | SheetHeader,
135 | SheetFooter,
136 | SheetTitle,
137 | SheetDescription,
138 | }
139 |
--------------------------------------------------------------------------------
/client/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/client/src/components/api-test/request-builder.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { Button } from "@/components/ui/button";
3 | import { Input } from "@/components/ui/input";
4 | import {
5 | Form,
6 | FormControl,
7 | FormField,
8 | FormItem,
9 | FormLabel,
10 | FormMessage,
11 | } from "@/components/ui/form";
12 | import {
13 | Select,
14 | SelectContent,
15 | SelectItem,
16 | SelectTrigger,
17 | SelectValue,
18 | } from "@/components/ui/select";
19 | import { Textarea } from "@/components/ui/textarea";
20 | import { useMutation, useQueryClient } from "@tanstack/react-query";
21 | import { createApiTest } from "@/lib/api";
22 | import { useToast } from "@/hooks/use-toast";
23 |
24 | export function RequestBuilder() {
25 | const queryClient = useQueryClient();
26 | const { toast } = useToast();
27 |
28 | const form = useForm({
29 | defaultValues: {
30 | endpoint: "/api/tasks",
31 | method: "GET",
32 | requestBody: "",
33 | },
34 | });
35 |
36 | const mutation = useMutation({
37 | mutationFn: async (values: {
38 | endpoint: string;
39 | method: string;
40 | requestBody: string;
41 | }) => {
42 | try {
43 | const response = await fetch(values.endpoint, {
44 | method: values.method,
45 | headers: {
46 | "Content-Type": "application/json",
47 | },
48 | body:
49 | values.method !== "GET" && values.requestBody
50 | ? values.requestBody
51 | : undefined,
52 | });
53 |
54 | const responseBody = await response.text();
55 | const test = {
56 | endpoint: values.endpoint,
57 | method: values.method,
58 | requestBody: values.requestBody,
59 | responseStatus: response.status,
60 | responseBody: responseBody,
61 | };
62 |
63 | return createApiTest(test);
64 | } catch (error) {
65 | throw new Error("Failed to execute API test");
66 | }
67 | },
68 | onSuccess: () => {
69 | queryClient.invalidateQueries({ queryKey: ["/api/tests"] });
70 | toast({
71 | title: "API Test Executed",
72 | description: "The API test has been successfully recorded.",
73 | });
74 | },
75 | });
76 |
77 | return (
78 |
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/client/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 |
7 | const AlertDialog = AlertDialogPrimitive.Root
8 |
9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10 |
11 | const AlertDialogPortal = AlertDialogPrimitive.Portal
12 |
13 | const AlertDialogOverlay = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, ...props }, ref) => (
17 |
25 | ))
26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27 |
28 | const AlertDialogContent = React.forwardRef<
29 | React.ElementRef,
30 | React.ComponentPropsWithoutRef
31 | >(({ className, ...props }, ref) => (
32 |
33 |
34 |
42 |
43 | ))
44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45 |
46 | const AlertDialogHeader = ({
47 | className,
48 | ...props
49 | }: React.HTMLAttributes) => (
50 |
57 | )
58 | AlertDialogHeader.displayName = "AlertDialogHeader"
59 |
60 | const AlertDialogFooter = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | AlertDialogFooter.displayName = "AlertDialogFooter"
73 |
74 | const AlertDialogTitle = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef
77 | >(({ className, ...props }, ref) => (
78 |
83 | ))
84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85 |
86 | const AlertDialogDescription = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
95 | ))
96 | AlertDialogDescription.displayName =
97 | AlertDialogPrimitive.Description.displayName
98 |
99 | const AlertDialogAction = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110 |
111 | const AlertDialogCancel = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
124 | ))
125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126 |
127 | export {
128 | AlertDialog,
129 | AlertDialogPortal,
130 | AlertDialogOverlay,
131 | AlertDialogTrigger,
132 | AlertDialogContent,
133 | AlertDialogHeader,
134 | AlertDialogFooter,
135 | AlertDialogTitle,
136 | AlertDialogDescription,
137 | AlertDialogAction,
138 | AlertDialogCancel,
139 | }
140 |
--------------------------------------------------------------------------------
/client/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/client/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { type DialogProps } from "@radix-ui/react-dialog"
3 | import { Command as CommandPrimitive } from "cmdk"
4 | import { Search } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { Dialog, DialogContent } from "@/components/ui/dialog"
8 |
9 | const Command = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 | ))
22 | Command.displayName = CommandPrimitive.displayName
23 |
24 | interface CommandDialogProps extends DialogProps {}
25 |
26 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/client/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/client/src/components/tasks/task-form.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { Task } from "@db/schema";
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import {
6 | Form,
7 | FormControl,
8 | FormField,
9 | FormItem,
10 | FormLabel,
11 | FormMessage,
12 | } from "@/components/ui/form";
13 | import {
14 | Select,
15 | SelectContent,
16 | SelectItem,
17 | SelectTrigger,
18 | SelectValue,
19 | } from "@/components/ui/select";
20 | import { Textarea } from "@/components/ui/textarea";
21 | import { useMutation, useQueryClient } from "@tanstack/react-query";
22 | import { createTask, updateTask } from "@/lib/api";
23 | import { useToast } from "@/hooks/use-toast";
24 |
25 | interface TaskFormProps {
26 | initialData?: Task;
27 | }
28 |
29 | export function TaskForm({ initialData }: TaskFormProps) {
30 | const queryClient = useQueryClient();
31 | const { toast } = useToast();
32 |
33 | const form = useForm({
34 | defaultValues: initialData || {
35 | title: "",
36 | description: "",
37 | status: "pending",
38 | priority: "medium",
39 | assignedTo: "",
40 | },
41 | });
42 |
43 | const mutation = useMutation({
44 | mutationFn: (data: Partial) => {
45 | if (initialData) {
46 | return updateTask(initialData.id, data);
47 | }
48 | return createTask(data as Omit);
49 | },
50 | onSuccess: () => {
51 | queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
52 | form.reset();
53 | toast({
54 | title: initialData ? "Task updated" : "Task created",
55 | description: `The task has been successfully ${
56 | initialData ? "updated" : "created"
57 | }.`,
58 | });
59 | },
60 | });
61 |
62 | return (
63 |
167 |
168 | );
169 | }
--------------------------------------------------------------------------------
/client/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SelectPrimitive from "@radix-ui/react-select"
3 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Select = SelectPrimitive.Root
8 |
9 | const SelectGroup = SelectPrimitive.Group
10 |
11 | const SelectValue = SelectPrimitive.Value
12 |
13 | const SelectTrigger = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, children, ...props }, ref) => (
17 | span]:line-clamp-1",
21 | className
22 | )}
23 | {...props}
24 | >
25 | {children}
26 |
27 |
28 |
29 |
30 | ))
31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
32 |
33 | const SelectScrollUpButton = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 |
46 |
47 | ))
48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
49 |
50 | const SelectScrollDownButton = React.forwardRef<
51 | React.ElementRef,
52 | React.ComponentPropsWithoutRef
53 | >(({ className, ...props }, ref) => (
54 |
62 |
63 |
64 | ))
65 | SelectScrollDownButton.displayName =
66 | SelectPrimitive.ScrollDownButton.displayName
67 |
68 | const SelectContent = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, children, position = "popper", ...props }, ref) => (
72 |
73 |
84 |
85 |
92 | {children}
93 |
94 |
95 |
96 |
97 | ))
98 | SelectContent.displayName = SelectPrimitive.Content.displayName
99 |
100 | const SelectLabel = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ))
110 | SelectLabel.displayName = SelectPrimitive.Label.displayName
111 |
112 | const SelectItem = React.forwardRef<
113 | React.ElementRef,
114 | React.ComponentPropsWithoutRef
115 | >(({ className, children, ...props }, ref) => (
116 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | {children}
131 |
132 | ))
133 | SelectItem.displayName = SelectPrimitive.Item.displayName
134 |
135 | const SelectSeparator = React.forwardRef<
136 | React.ElementRef,
137 | React.ComponentPropsWithoutRef
138 | >(({ className, ...props }, ref) => (
139 |
144 | ))
145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
146 |
147 | export {
148 | Select,
149 | SelectGroup,
150 | SelectValue,
151 | SelectTrigger,
152 | SelectContent,
153 | SelectLabel,
154 | SelectItem,
155 | SelectSeparator,
156 | SelectScrollUpButton,
157 | SelectScrollDownButton,
158 | }
159 |
--------------------------------------------------------------------------------
/client/src/pages/metrics-dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { Link } from "wouter";
3 | import { Button } from "@/components/ui/button";
4 | import { Card } from "@/components/ui/card";
5 | import { LayoutDashboard, BarChart2 } from "lucide-react";
6 | import {
7 | LineChart,
8 | Line,
9 | BarChart,
10 | Bar,
11 | XAxis,
12 | YAxis,
13 | CartesianGrid,
14 | Tooltip,
15 | Legend,
16 | ResponsiveContainer,
17 | } from "recharts";
18 | import { format } from "date-fns";
19 | import { Metric } from "@db/schema";
20 |
21 | async function fetchMetrics(): Promise {
22 | const response = await fetch("/api/metrics");
23 | return response.json();
24 | }
25 |
26 | function calculateMetrics(metrics: Metric[]) {
27 | const endpointStats = metrics.reduce((acc, metric) => {
28 | const key = `${metric.method} ${metric.endpoint}`;
29 | if (!acc[key]) {
30 | acc[key] = {
31 | totalTime: 0,
32 | count: 0,
33 | errors: 0,
34 | times: [],
35 | };
36 | }
37 | acc[key].totalTime += metric.responseTime;
38 | acc[key].count += 1;
39 | if (metric.responseStatus >= 400) {
40 | acc[key].errors += 1;
41 | }
42 | acc[key].times.push({
43 | time: new Date(metric.timestamp).getTime(),
44 | responseTime: metric.responseTime,
45 | });
46 | return acc;
47 | }, {} as Record }>);
48 |
49 | return Object.entries(endpointStats).map(([endpoint, stats]) => ({
50 | endpoint,
51 | averageResponseTime: stats.totalTime / stats.count,
52 | errorRate: (stats.errors / stats.count) * 100,
53 | requestCount: stats.count,
54 | responseTimes: stats.times.sort((a, b) => a.time - b.time),
55 | }));
56 | }
57 |
58 | export default function MetricsDashboard() {
59 | const { data: metrics = [] } = useQuery({
60 | queryKey: ["/api/metrics"],
61 | queryFn: fetchMetrics,
62 | refetchInterval: 5000, // Refresh every 5 seconds
63 | });
64 |
65 | const processedMetrics = calculateMetrics(metrics);
66 |
67 | return (
68 |
69 |
89 |
90 |
91 |
92 |
93 | Average Response Times
94 |
95 |
96 |
97 |
98 |
104 |
111 |
112 |
113 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | Error Rates
125 |
126 |
127 |
128 |
129 |
135 |
142 |
143 |
144 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | Response Time Trends
156 |
157 |
158 |
159 |
160 | format(time, "HH:mm:ss")}
165 | />
166 |
173 | format(time, "HH:mm:ss")}
175 | />
176 |
177 | {processedMetrics.map((metric, index) => (
178 |
186 | ))}
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | );
195 | }
196 |
--------------------------------------------------------------------------------
/client/src/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import useEmblaCarousel, {
3 | type UseEmblaCarouselType,
4 | } from "embla-carousel-react"
5 | import { ArrowLeft, ArrowRight } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { Button } from "@/components/ui/button"
9 |
10 | type CarouselApi = UseEmblaCarouselType[1]
11 | type UseCarouselParameters = Parameters
12 | type CarouselOptions = UseCarouselParameters[0]
13 | type CarouselPlugin = UseCarouselParameters[1]
14 |
15 | type CarouselProps = {
16 | opts?: CarouselOptions
17 | plugins?: CarouselPlugin
18 | orientation?: "horizontal" | "vertical"
19 | setApi?: (api: CarouselApi) => void
20 | }
21 |
22 | type CarouselContextProps = {
23 | carouselRef: ReturnType[0]
24 | api: ReturnType[1]
25 | scrollPrev: () => void
26 | scrollNext: () => void
27 | canScrollPrev: boolean
28 | canScrollNext: boolean
29 | } & CarouselProps
30 |
31 | const CarouselContext = React.createContext(null)
32 |
33 | function useCarousel() {
34 | const context = React.useContext(CarouselContext)
35 |
36 | if (!context) {
37 | throw new Error("useCarousel must be used within a ")
38 | }
39 |
40 | return context
41 | }
42 |
43 | const Carousel = React.forwardRef<
44 | HTMLDivElement,
45 | React.HTMLAttributes & CarouselProps
46 | >(
47 | (
48 | {
49 | orientation = "horizontal",
50 | opts,
51 | setApi,
52 | plugins,
53 | className,
54 | children,
55 | ...props
56 | },
57 | ref
58 | ) => {
59 | const [carouselRef, api] = useEmblaCarousel(
60 | {
61 | ...opts,
62 | axis: orientation === "horizontal" ? "x" : "y",
63 | },
64 | plugins
65 | )
66 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
67 | const [canScrollNext, setCanScrollNext] = React.useState(false)
68 |
69 | const onSelect = React.useCallback((api: CarouselApi) => {
70 | if (!api) {
71 | return
72 | }
73 |
74 | setCanScrollPrev(api.canScrollPrev())
75 | setCanScrollNext(api.canScrollNext())
76 | }, [])
77 |
78 | const scrollPrev = React.useCallback(() => {
79 | api?.scrollPrev()
80 | }, [api])
81 |
82 | const scrollNext = React.useCallback(() => {
83 | api?.scrollNext()
84 | }, [api])
85 |
86 | const handleKeyDown = React.useCallback(
87 | (event: React.KeyboardEvent) => {
88 | if (event.key === "ArrowLeft") {
89 | event.preventDefault()
90 | scrollPrev()
91 | } else if (event.key === "ArrowRight") {
92 | event.preventDefault()
93 | scrollNext()
94 | }
95 | },
96 | [scrollPrev, scrollNext]
97 | )
98 |
99 | React.useEffect(() => {
100 | if (!api || !setApi) {
101 | return
102 | }
103 |
104 | setApi(api)
105 | }, [api, setApi])
106 |
107 | React.useEffect(() => {
108 | if (!api) {
109 | return
110 | }
111 |
112 | onSelect(api)
113 | api.on("reInit", onSelect)
114 | api.on("select", onSelect)
115 |
116 | return () => {
117 | api?.off("select", onSelect)
118 | }
119 | }, [api, onSelect])
120 |
121 | return (
122 |
135 |
143 | {children}
144 |
145 |
146 | )
147 | }
148 | )
149 | Carousel.displayName = "Carousel"
150 |
151 | const CarouselContent = React.forwardRef<
152 | HTMLDivElement,
153 | React.HTMLAttributes
154 | >(({ className, ...props }, ref) => {
155 | const { carouselRef, orientation } = useCarousel()
156 |
157 | return (
158 |
169 | )
170 | })
171 | CarouselContent.displayName = "CarouselContent"
172 |
173 | const CarouselItem = React.forwardRef<
174 | HTMLDivElement,
175 | React.HTMLAttributes
176 | >(({ className, ...props }, ref) => {
177 | const { orientation } = useCarousel()
178 |
179 | return (
180 |
191 | )
192 | })
193 | CarouselItem.displayName = "CarouselItem"
194 |
195 | const CarouselPrevious = React.forwardRef<
196 | HTMLButtonElement,
197 | React.ComponentProps
198 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
199 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
200 |
201 | return (
202 |
220 | )
221 | })
222 | CarouselPrevious.displayName = "CarouselPrevious"
223 |
224 | const CarouselNext = React.forwardRef<
225 | HTMLButtonElement,
226 | React.ComponentProps
227 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
228 | const { orientation, scrollNext, canScrollNext } = useCarousel()
229 |
230 | return (
231 |
249 | )
250 | })
251 | CarouselNext.displayName = "CarouselNext"
252 |
253 | export {
254 | type CarouselApi,
255 | Carousel,
256 | CarouselContent,
257 | CarouselItem,
258 | CarouselPrevious,
259 | CarouselNext,
260 | }
261 |
--------------------------------------------------------------------------------
/client/src/components/ui/context-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
3 | import { Check, ChevronRight, Circle } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const ContextMenu = ContextMenuPrimitive.Root
8 |
9 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger
10 |
11 | const ContextMenuGroup = ContextMenuPrimitive.Group
12 |
13 | const ContextMenuPortal = ContextMenuPrimitive.Portal
14 |
15 | const ContextMenuSub = ContextMenuPrimitive.Sub
16 |
17 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
18 |
19 | const ContextMenuSubTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef & {
22 | inset?: boolean
23 | }
24 | >(({ className, inset, children, ...props }, ref) => (
25 |
34 | {children}
35 |
36 |
37 | ))
38 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
39 |
40 | const ContextMenuSubContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
54 |
55 | const ContextMenuContent = React.forwardRef<
56 | React.ElementRef,
57 | React.ComponentPropsWithoutRef
58 | >(({ className, ...props }, ref) => (
59 |
60 |
68 |
69 | ))
70 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
71 |
72 | const ContextMenuItem = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef & {
75 | inset?: boolean
76 | }
77 | >(({ className, inset, ...props }, ref) => (
78 |
87 | ))
88 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
89 |
90 | const ContextMenuCheckboxItem = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, children, checked, ...props }, ref) => (
94 |
103 |