feat: add internal API test harness

This commit introduces a new internal API test harness built with React and Vite. This harness provides a user interface for testing various API endpoints, including authentication, core integrations, and entity-related APIs.

The harness includes the following features:

- Firebase authentication integration for secure API testing.
- A modular design with separate components for different API categories.
- Form-based input for API parameters, allowing users to easily configure requests.
- JSON-based response display for clear and readable API results.
- Error handling and display for debugging purposes.
- A navigation system for easy access to different API endpoints.
- Environment-specific configuration for testing in different environments.

This harness will enable developers to quickly and efficiently test API endpoints, ensuring the quality and reliability of the KROW backend services.

The following files were added:

- Makefile: Added targets for installing, developing, building, and deploying the API test harness.
- firebase.json: Added hosting configurations for the API test harness in development and staging environments.
- firebase/internal-launchpad/index.html: Updated with accordion styles and navigation for diagrams and documents.
- internal-api-harness/.env.example: Example environment variables for the API test harness.
- internal-api-harness/.gitignore: Git ignore file for the API test harness.
- internal-api-harness/README.md: README file for the API test harness.
- internal-api-harness/components.json: Configuration file for shadcn-ui components.
- internal-api-harness/eslint.config.js: ESLint configuration file.
- internal-api-harness/index.html: Main HTML file for the API test harness.
- internal-api-harness/jsconfig.json: JSConfig file for the API test harness.
- internal-api-harness/package.json: Package file for the API test harness.
- internal-api-harness/postcss.config.js: PostCSS configuration file.
- internal-api-harness/public/logo.svg: Krow logo.
- internal-api-harness/public/vite.svg: Vite logo.
- internal-api-harness/src/App.css: CSS file for the App component.
- internal-api-harness/src/App.jsx: Main App component.
- internal-api-harness/src/api/client.js: API client for making requests to the backend.
- internal-api-harness/src/api/krowSDK.js: SDK for interacting with Krow APIs.
- internal-api-harness/src/assets/react.svg: React logo.
- internal-api-harness/src/components/ApiResponse.jsx: Component for displaying API responses.
- internal-api-harness/src/components/Layout.jsx: Layout component for the API test harness.
- internal-api-harness/src/components/ServiceTester.jsx: Component for testing individual services.
- internal-api-harness/src/components/ui/button.jsx: Button component.
- internal-api-harness/src/components/ui/card.jsx: Card component.
- internal-api-harness/src/components/ui/collapsible.jsx: Collapsible component.
- internal-api-harness/src/components/ui/input.jsx: Input component.
- internal-api-harness/src/components/ui/label.jsx: Label component.
- internal-api-harness/src/components/ui/select.jsx: Select component.
- internal-api-harness/src/components/ui/textarea.jsx: Textarea component.
- internal-api-harness/src/firebase.js: Firebase configuration file.
- internal-api-harness/src/index.css: Main CSS file.
- internal-api-harness/src/lib/utils.js: Utility functions.
- internal-api-harness/src/main.jsx: Main entry point for the React application.
- internal-api-harness/src/pages/ApiPlaceholder.jsx: Placeholder component for unimplemented APIs.
- internal-api-harness/src/pages/EntityTester.jsx: Component for testing entity APIs.
- internal-api-harness/src/pages/GenerateImage.jsx: Component for testing the Generate Image API.
- internal-api-harness/src/pages/Home.jsx: Home page component.
- internal-api-harness/src/pages/Login.jsx: Login page component.
- internal-api-harness/src/pages/auth/GetMe.jsx: Component for testing the Get Me API.
- internal-api-harness/src/pages/core/CreateSignedUrl.jsx: Component for testing the Create Signed URL API.
- internal-api-harness/src/pages/core/InvokeLLM.jsx: Component for testing the Invoke LLM API.
- internal-api-harness/src/pages/core/SendEmail.jsx: Component for testing the Send Email API.
- internal-api-harness/src/pages/core/UploadFile.jsx: Component for testing the Upload File API.
- internal-api-harness/src/pages/core/UploadPrivateFile.jsx: Component for testing the Upload Private File API.
- internal-api-harness/tailwind.config.js: Tailwind CSS configuration file.
- internal-api-harness/vite.config.js: Vite configuration file.
This commit is contained in:
bwnyasse
2025-11-16 21:45:17 -05:00
parent 831570f2e0
commit f7c2027065
47 changed files with 8707 additions and 154 deletions

View File

@@ -0,0 +1,38 @@
const ApiResponse = ({ response, error, loading }) => {
if (loading) {
return (
<div className="mt-4">
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
<div className="bg-slate-100 border border-slate-200 rounded-lg p-4">
<p className="text-slate-500">Loading...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="mt-4">
<h3 className="text-lg font-semibold text-red-800 mb-2">Error</h3>
<pre className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 text-sm whitespace-pre-wrap">
{JSON.stringify(error, null, 2)}
</pre>
</div>
);
}
if (response) {
return (
<div className="mt-4">
<h3 className="text-lg font-semibold text-slate-800 mb-2">Response</h3>
<pre className="bg-slate-100 border border-slate-200 rounded-lg p-4 text-sm whitespace-pre-wrap">
{JSON.stringify(response, null, 2)}
</pre>
</div>
);
}
return null;
};
export default ApiResponse;

View File

@@ -0,0 +1,125 @@
import { Link, Navigate, Outlet, useLocation } from "react-router-dom";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth } from "../firebase";
import { krowSDK } from "@/api/krowSDK";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
const navLinks = {
auth: [
{ path: "/auth/me", title: "Get Me" },
{ path: "/auth/update-me", title: "Update Me" },
],
core: [
{ path: "/core/send-email", title: "Send Email" },
{ path: "/core/invoke-llm", title: "Invoke LLM" },
{ path: "/core/upload-file", title: "Upload File" },
{ path: "/core/upload-private-file", title: "Upload Private File" },
{ path: "/core/create-signed-url", title: "Create Signed URL" },
{ path: "/core/extract-data", title: "Extract Data from File" },
{ path: "/core/generate-image", title: "Generate Image" },
],
entities: [
{ path: "/entities", title: "Entity API Tester" }
]
};
const NavSection = ({ title, links }) => {
const location = useLocation();
const [isOpen, setIsOpen] = useState(true);
const navLinkClasses = (path) =>
`flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
location.pathname === path
? "bg-slate-200 text-slate-900"
: "text-slate-600 hover:bg-slate-100"
}`;
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="w-full">
<div className="flex items-center justify-between px-3 py-2">
<h2 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
{title}
</h2>
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1">
{links.map((api) => (
<Link key={api.path} to={api.path} className={navLinkClasses(api.path)}>
{api.title}
</Link>
))}
</CollapsibleContent>
</Collapsible>
);
}
const Layout = () => {
const [user, loading] = useAuthState(auth);
const location = useLocation();
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<div>Loading...</div>
</div>
);
}
if (!user) {
return <Navigate to="/login" />;
}
const handleLogout = () => {
krowSDK.auth.logout();
};
return (
<div className="flex h-screen bg-slate-50">
{/* Sidebar */}
<aside className="w-72 bg-white border-r border-slate-200 flex flex-col">
<div className="p-6 border-b border-slate-200">
<div className="flex items-center space-x-3">
<img src="/logo.svg" alt="Krow Logo" className="h-12 w-12" />
<div>
<h1 className="text-xl font-bold text-slate-900">KROW</h1>
<p className="text-xs text-slate-500">API Test Harness ({import.meta.env.VITE_HARNESS_ENVIRONMENT})</p>
</div>
</div>
</div>
<nav className="flex-1 overflow-y-auto p-4 space-y-2">
<NavSection title="Auth" links={navLinks.auth} />
<NavSection title="Core Integrations" links={navLinks.core} />
<NavSection title="Entities" links={navLinks.entities} />
</nav>
<div className="p-4 border-t border-slate-200">
<div className="text-sm text-slate-600 truncate mb-2" title={user.email}>{user.email}</div>
<Button onClick={handleLogout} className="w-full">
Logout
</Button>
</div>
</aside>
{/* Main Content */}
<main className="flex-1 overflow-y-auto">
<div className="p-8">
<Outlet />
</div>
</main>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,120 @@
import { useState } from "react";
import apiClient from "../api/client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
const ServiceTester = ({ serviceName, serviceDescription, endpoint, fields }) => {
const [formData, setFormData] = useState({});
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
const { name, value, files } = e.target;
if (files) {
setFormData({ ...formData, [name]: files[0] });
} else {
setFormData({ ...formData, [name]: value });
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse(null);
const data = new FormData();
for (const key in formData) {
data.append(key, formData[key]);
}
try {
const res = await apiClient.post(endpoint, data);
setResponse(res.data);
} catch (err) {
setError(err.response?.data || err.message);
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-slate-900">{serviceName}</h1>
<p className="text-slate-600 mt-1">{serviceDescription}</p>
</div>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle>Parameters</CardTitle>
<CardDescription>
Fill in the required parameters to execute the service.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{fields.map((field) => (
<div key={field.name} className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor={field.name}>{field.label}</Label>
{field.type === "textarea" ? (
<Textarea
id={field.name}
name={field.name}
onChange={handleChange}
placeholder={field.placeholder}
/>
) : (
<Input
id={field.name}
name={field.name}
type={field.type}
onChange={handleChange}
placeholder={field.placeholder}
/>
)}
</div>
))}
</CardContent>
<CardFooter>
<Button type="submit" disabled={loading}>
{loading ? "Executing..." : "Execute"}
</Button>
</CardFooter>
</Card>
</form>
{response && (
<div className="mt-8">
<h2 className="text-xl font-bold text-slate-900 mb-2">Response</h2>
<Card>
<CardContent className="p-4">
<pre className="text-sm overflow-x-auto bg-slate-900 text-white p-4 rounded-md">
{JSON.stringify(response, null, 2)}
</pre>
</CardContent>
</Card>
</div>
)}
{error && (
<div className="mt-8">
<h2 className="text-xl font-bold text-red-600 mb-2">Error</h2>
<Card className="border-red-500">
<CardContent className="p-4">
<pre className="text-sm overflow-x-auto bg-red-50 text-red-800 p-4 rounded-md">
{JSON.stringify(error, null, 2)}
</pre>
</CardContent>
</Card>
</div>
)}
</div>
);
};
export default ServiceTester;

View File

@@ -0,0 +1,48 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,50 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props} />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props} />
);
})
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,16 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,105 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props} />
);
})
Textarea.displayName = "Textarea"
export { Textarea }