From ab5883a9934147f9f1eded3be97226e25badcbf2 Mon Sep 17 00:00:00 2001 From: yariv-h Date: Thu, 17 Jul 2025 10:20:11 +0300 Subject: [PATCH 1/2] Adding local dev script for running backend & frontend locally alongside with docker containers for local development --- apps/opik-backend/entrypoint.sh | 0 apps/opik-backend/install_rds_cert.sh | 0 apps/opik-backend/run_db_migrations.sh | 0 apps/opik-guardrails-backend/entrypoint.sh | 0 .../demo_data_entrypoint.sh | 0 scripts/QUICKSTART.md | 77 +++ scripts/README-dev-local.md | 183 +++++++ scripts/dev-local.sh | 447 ++++++++++++++++++ 8 files changed, 707 insertions(+) mode change 100644 => 100755 apps/opik-backend/entrypoint.sh mode change 100644 => 100755 apps/opik-backend/install_rds_cert.sh mode change 100644 => 100755 apps/opik-backend/run_db_migrations.sh mode change 100644 => 100755 apps/opik-guardrails-backend/entrypoint.sh mode change 100644 => 100755 apps/opik-python-backend/demo_data_entrypoint.sh create mode 100644 scripts/QUICKSTART.md create mode 100644 scripts/README-dev-local.md create mode 100755 scripts/dev-local.sh diff --git a/apps/opik-backend/entrypoint.sh b/apps/opik-backend/entrypoint.sh old mode 100644 new mode 100755 diff --git a/apps/opik-backend/install_rds_cert.sh b/apps/opik-backend/install_rds_cert.sh old mode 100644 new mode 100755 diff --git a/apps/opik-backend/run_db_migrations.sh b/apps/opik-backend/run_db_migrations.sh old mode 100644 new mode 100755 diff --git a/apps/opik-guardrails-backend/entrypoint.sh b/apps/opik-guardrails-backend/entrypoint.sh old mode 100644 new mode 100755 diff --git a/apps/opik-python-backend/demo_data_entrypoint.sh b/apps/opik-python-backend/demo_data_entrypoint.sh old mode 100644 new mode 100755 diff --git a/scripts/QUICKSTART.md b/scripts/QUICKSTART.md new file mode 100644 index 0000000000..b1f1b3de1a --- /dev/null +++ b/scripts/QUICKSTART.md @@ -0,0 +1,77 @@ +# Quick Start Guide + +## Getting Started with OPIK Local Development + +This guide will help you get the OPIK development environment running locally in minutes. + +### Prerequisites + +Make sure you have the following installed: +- Docker +- Maven +- Node.js (v18+) +- npm + +### Step 1: Start Everything + +Run the development script: + +```bash +./scripts/dev-local.sh +``` + +This will: +- ✅ Start all required containers (MySQL, Redis, ClickHouse, etc.) +- ✅ Configure nginx and frontend environment for local development +- ✅ Build the backend with Maven (skipping tests) +- ✅ Start the backend on http://localhost:8080 +- ✅ Start the frontend on http://localhost:5173 + +### Step 2: Access the Application + +Once everything is running, you can access: +- **Frontend**: http://localhost:5173 +- **Backend API**: http://localhost:8080 +- **Backend Health Check**: http://localhost:8080/health-check + +### Step 3: Stop Everything + +Press `Ctrl+C` to stop the backend and frontend processes. + +To stop the containers: +```bash +./opik.sh --stop +``` + +### Alternative Workflows + +#### Start Only Containers +If you want to run backend/frontend from your IDE: +```bash +./scripts/dev-local.sh --containers-only +``` + +#### Start Only Backend +```bash +./scripts/dev-local.sh --backend-only +``` + +#### Start Only Frontend +```bash +./scripts/dev-local.sh --frontend-only +``` + +### Troubleshooting + +If you encounter issues: + +1. **Check if Docker is running** +2. **Check if ports are available** (8080, 5173, 3306, 6379, 8123) +3. **Check container logs**: `docker logs opik-mysql-1` +4. **Restart containers**: `./opik.sh --stop && ./scripts/dev-local.sh --containers-only` + +### Next Steps + +- Read the full documentation in `scripts/README-dev-local.md` +- Check the main project README for development guidelines +- Join the community for support \ No newline at end of file diff --git a/scripts/README-dev-local.md b/scripts/README-dev-local.md new file mode 100644 index 0000000000..a5d8eed11c --- /dev/null +++ b/scripts/README-dev-local.md @@ -0,0 +1,183 @@ +# Local Development Script + +This directory contains a script for running OPIK locally for development purposes. + +## `dev-local.sh` + +A comprehensive shell script that sets up the OPIK development environment by: +1. Starting all required containers (excluding backend & frontend) +2. Building the backend with Maven (skipping tests) +3. Running the backend and frontend locally + +### Prerequisites + +Before running the script, ensure you have the following installed: + +- **Docker** - For running containers +- **Maven** - For building the Java backend +- **Node.js** - For running the frontend +- **npm** - For managing frontend dependencies + +### Usage + +#### Basic Usage (Full Setup) +```bash +./scripts/dev-local.sh +``` + +This will: +- Start all required containers (MySQL, Redis, ClickHouse, Zookeeper, MinIO, Python Backend) +- Build the backend with Maven (skipping tests) +- Run the backend locally on http://localhost:8080 +- Run the frontend locally on http://localhost:5173 + +#### Options + +```bash +# Only start containers (don't run backend/frontend locally) +./scripts/dev-local.sh --containers-only + +# Only build and run backend locally +./scripts/dev-local.sh --backend-only + +# Only run frontend locally +./scripts/dev-local.sh --frontend-only + +# Show help +./scripts/dev-local.sh --help +``` + +### What the Script Does + +#### Container Management +- Starts the following containers using Docker Compose: + - `mysql` - Database for state management + - `redis` - Caching and session storage + - `clickhouse` - Analytics database + - `zookeeper` - Distributed coordination + - `minio` - Object storage (S3-compatible) + - `python-backend` - Python evaluation service + +- Configures nginx for local development (updates `nginx_default_local.conf`) +- Waits for all containers to be healthy before proceeding +- Uses the same container configuration as the main `opik.sh` script + +#### Backend Setup +- Changes to the `apps/opik-backend` directory +- Runs `mvn clean install -DskipTests` to build the backend +- Sets up all necessary environment variables for local development +- Runs database migrations using `./run_db_migrations.sh` +- Starts the backend server using the built JAR file +- Waits for the backend to be accessible on http://localhost:8080 + +#### Frontend Setup +- Changes to the `apps/opik-frontend` directory +- Installs npm dependencies if `node_modules` doesn't exist +- Configures `.env.development` with local development settings: + - `VITE_BASE_URL=/` + - `VITE_BASE_API_URL=http://localhost:8080` +- Starts the Vite development server using `npm start` +- Waits for the frontend to be accessible on http://localhost:5173 + +### Configuration Files + +The script automatically configures the following files for local development: + +#### Frontend Environment (`.env.development`) +```bash +VITE_BASE_URL=/ +VITE_BASE_API_URL=http://localhost:8080 +``` + +#### Nginx Configuration (`nginx_default_local.conf`) +Updates the proxy_pass directive to use `host.docker.internal:8080` instead of `backend:8080` for local development. + +### Environment Variables + +The script sets up the following environment variables for the backend: + +```bash +STATE_DB_PROTOCOL=jdbc:mysql:// +STATE_DB_URL=localhost:3306/opik?createDatabaseIfNotExist=true&rewriteBatchedStatements=true +STATE_DB_DATABASE_NAME=opik +STATE_DB_USER=opik +STATE_DB_PASS=opik +ANALYTICS_DB_MIGRATIONS_URL=jdbc:clickhouse://localhost:8123 +ANALYTICS_DB_MIGRATIONS_USER=opik +ANALYTICS_DB_MIGRATIONS_PASS=opik +ANALYTICS_DB_PROTOCOL=HTTP +ANALYTICS_DB_HOST=localhost +ANALYTICS_DB_PORT=8123 +ANALYTICS_DB_DATABASE_NAME=opik +ANALYTICS_DB_USERNAME=opik +ANALYTICS_DB_PASS=opik +JAVA_OPTS=-Dliquibase.propertySubstitutionEnabled=true -XX:+UseG1GC -XX:MaxRAMPercentage=80.0 +REDIS_URL=redis://:opik@localhost:6379/ +OPIK_OTEL_SDK_ENABLED=false +OTEL_VERSION=2.16.0 +OTEL_PROPAGATORS=tracecontext,baggage,b3 +OTEL_EXPERIMENTAL_EXPORTER_OTLP_RETRY_ENABLED=true +OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION=BASE2_EXPONENTIAL_BUCKET_HISTOGRAM +OTEL_EXPERIMENTAL_RESOURCE_DISABLED_KEYS=process.command_args +OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta +OPIK_USAGE_REPORT_ENABLED=true +AWS_ACCESS_KEY_ID=THAAIOSFODNN7EXAMPLE +AWS_SECRET_ACCESS_KEY=LESlrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +PYTHON_EVALUATOR_URL=http://localhost:8000 +TOGGLE_GUARDRAILS_ENABLED=false +``` + +### Stopping the Services + +- Press `Ctrl+C` to stop all services gracefully +- The script will automatically clean up background processes +- Containers will continue running (use `./opik.sh --stop` to stop them) + +### Troubleshooting + +#### Common Issues + +1. **Docker not running** + ``` + [ERROR] Docker is not running or not accessible. Please start Docker first. + ``` + - Start Docker Desktop or Docker daemon + +2. **Maven not found** + ``` + [ERROR] Maven is not installed. Please install Maven first. + ``` + - Install Maven: `sudo apt install maven` (Ubuntu/Debian) or `brew install maven` (macOS) + +3. **Node.js not found** + ``` + [ERROR] Node.js is not installed. Please install Node.js first. + ``` + - Install Node.js from https://nodejs.org/ + +4. **Backend build fails** + - Check that you have Java 17+ installed + - Ensure you have sufficient memory for Maven build + - Check the Maven output for specific error messages + +5. **Containers not starting** + - Check Docker logs: `docker logs opik-mysql-1` (replace with container name) + - Ensure ports are not already in use + - Check available disk space + +#### Debugging + +- The script provides colored output to help identify issues +- Check container health: `docker ps` to see running containers +- Check container logs: `docker logs ` +- Verify ports are accessible: `curl http://localhost:8080/health-check` + +### Integration with IDE + +You can use this script to start the infrastructure, then run the backend and frontend directly from your IDE: + +1. Run `./scripts/dev-local.sh --containers-only` to start containers +2. Run the backend from your IDE (e.g., IntelliJ IDEA, VS Code) +3. Run the frontend from your IDE or terminal + +This approach gives you better debugging capabilities and faster development cycles. \ No newline at end of file diff --git a/scripts/dev-local.sh b/scripts/dev-local.sh new file mode 100755 index 0000000000..d93417c182 --- /dev/null +++ b/scripts/dev-local.sh @@ -0,0 +1,447 @@ +#!/bin/bash + +# Local Development Script for OPIK +# This script starts all required containers (except backend & frontend) and runs them locally + +set -e # Exit on any error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Global variables +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to check if Docker is running +check_docker() { + if ! docker info >/dev/null 2>&1; then + print_error "Docker is not running or not accessible. Please start Docker first." + exit 1 + fi + print_success "Docker is running" +} + +# Function to check if required tools are installed +check_requirements() { + print_status "Checking requirements..." + + if ! command_exists docker; then + print_error "Docker is not installed. Please install Docker first." + exit 1 + fi + + if ! command_exists mvn; then + print_error "Maven is not installed. Please install Maven first." + exit 1 + fi + + if ! command_exists node; then + print_error "Node.js is not installed. Please install Node.js first." + exit 1 + fi + + if ! command_exists npm; then + print_error "npm is not installed. Please install npm first." + exit 1 + fi + + print_success "All requirements are met" +} + +# Function to configure nginx for local development +configure_nginx() { + print_status "Configuring nginx for local development..." + + nginx_config="$PROJECT_ROOT/deployment/docker-compose/nginx_default_local.conf" + + # Update nginx configuration to use host.docker.internal for backend + if grep -q "proxy_pass http://backend:8080;" "$nginx_config"; then + sed -i '' 's|proxy_pass http://backend:8080;|proxy_pass http://host.docker.internal:8080;|' "$nginx_config" + print_success "Nginx configuration updated for local development" + elif grep -q "proxy_pass http://host.docker.internal:8080;" "$nginx_config"; then + print_status "Nginx configuration already set for local development" + else + print_warning "Could not find expected proxy_pass configuration in nginx file" + fi +} + +# Function to start containers (excluding backend and frontend) +start_containers() { + print_status "Starting required containers (excluding backend and frontend)..." + + # Change to project root + cd "$PROJECT_ROOT" + + # Configure nginx for local development + configure_nginx + + # Start containers using docker compose with port mapping override + # We'll use docker compose directly to have more control + docker compose -f deployment/docker-compose/docker-compose.yaml -f deployment/docker-compose/docker-compose.override.yaml up -d mysql redis clickhouse zookeeper minio mc python-backend + + print_status "Waiting for containers to be healthy..." + + # Wait for containers to be healthy + local max_retries=60 + local interval=2 + + # Check MySQL + print_status "Waiting for MySQL..." + for i in $(seq 1 $max_retries); do + if docker inspect -f '{{.State.Health.Status}}' opik-mysql-1 2>/dev/null | grep -q "healthy"; then + print_success "MySQL is healthy" + break + fi + if [ $i -eq $max_retries ]; then + print_error "MySQL failed to become healthy after ${max_retries}s" + exit 1 + fi + sleep $interval + done + + # Check Redis + print_status "Waiting for Redis..." + for i in $(seq 1 $max_retries); do + if docker inspect -f '{{.State.Health.Status}}' opik-redis-1 2>/dev/null | grep -q "healthy"; then + print_success "Redis is healthy" + break + fi + if [ $i -eq $max_retries ]; then + print_error "Redis failed to become healthy after ${max_retries}s" + exit 1 + fi + sleep $interval + done + + # Check ClickHouse + print_status "Waiting for ClickHouse..." + for i in $(seq 1 $max_retries); do + if docker inspect -f '{{.State.Health.Status}}' opik-clickhouse-1 2>/dev/null | grep -q "healthy"; then + print_success "ClickHouse is healthy" + break + fi + if [ $i -eq $max_retries ]; then + print_error "ClickHouse failed to become healthy after ${max_retries}s" + exit 1 + fi + sleep $interval + done + + # Check Zookeeper + print_status "Waiting for Zookeeper..." + for i in $(seq 1 $max_retries); do + if docker inspect -f '{{.State.Health.Status}}' opik-zookeeper-1 2>/dev/null | grep -q "healthy"; then + print_success "Zookeeper is healthy" + break + fi + if [ $i -eq $max_retries ]; then + print_error "Zookeeper failed to become healthy after ${max_retries}s" + exit 1 + fi + sleep $interval + done + + # Check MinIO + print_status "Waiting for MinIO..." + for i in $(seq 1 $max_retries); do + if docker inspect -f '{{.State.Health.Status}}' opik-minio-1 2>/dev/null | grep -q "healthy"; then + print_success "MinIO is healthy" + break + fi + if [ $i -eq $max_retries ]; then + print_error "MinIO failed to become healthy after ${max_retries}s" + exit 1 + fi + sleep $interval + done + + # Check Python Backend + print_status "Waiting for Python Backend..." + for i in $(seq 1 $max_retries); do + if docker inspect -f '{{.State.Health.Status}}' opik-python-backend-1 2>/dev/null | grep -q "healthy"; then + print_success "Python Backend is healthy" + break + fi + if [ $i -eq $max_retries ]; then + print_warning "Python Backend may not be fully healthy, but continuing..." + break + fi + sleep $interval + done + + print_success "All required containers are running" +} + +# Function to build backend with Maven +build_backend() { + print_status "Building backend with Maven (skipping tests)..." + + backend_dir="$PROJECT_ROOT/apps/opik-backend" + + cd "$backend_dir" + + # Clean and install, skipping tests + mvn clean install -DskipTests + + if [ $? -eq 0 ]; then + print_success "Backend built successfully" + else + print_error "Backend build failed" + exit 1 + fi +} + +# Function to run backend locally +run_backend() { + print_status "Starting backend locally..." + + backend_dir="$PROJECT_ROOT/apps/opik-backend" + + cd "$backend_dir" + + # Set environment variables for local development + export CORS="true" + export STATE_DB_PROTOCOL="jdbc:mysql://" + export STATE_DB_URL="localhost:3306/opik?createDatabaseIfNotExist=true&rewriteBatchedStatements=true" + export STATE_DB_DATABASE_NAME="opik" + export STATE_DB_USER="opik" + export STATE_DB_PASS="opik" + export ANALYTICS_DB_MIGRATIONS_URL="jdbc:clickhouse://localhost:8123" + export ANALYTICS_DB_MIGRATIONS_USER="opik" + export ANALYTICS_DB_MIGRATIONS_PASS="opik" + export ANALYTICS_DB_PROTOCOL="HTTP" + export ANALYTICS_DB_HOST="localhost" + export ANALYTICS_DB_PORT="8123" + export ANALYTICS_DB_DATABASE_NAME="opik" + export ANALYTICS_DB_USERNAME="opik" + export ANALYTICS_DB_PASS="opik" + export JAVA_OPTS="-Dliquibase.propertySubstitutionEnabled=true -XX:+UseG1GC -XX:MaxRAMPercentage=80.0" + export REDIS_URL="redis://:opik@localhost:6379/" + export OPIK_OTEL_SDK_ENABLED="false" + export OTEL_VERSION="2.16.0" + export OTEL_PROPAGATORS="tracecontext,baggage,b3" + export OTEL_EXPERIMENTAL_EXPORTER_OTLP_RETRY_ENABLED="true" + export OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION="BASE2_EXPONENTIAL_BUCKET_HISTOGRAM" + export OTEL_EXPERIMENTAL_RESOURCE_DISABLED_KEYS="process.command_args" + export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE="delta" + export OPIK_USAGE_REPORT_ENABLED="true" + export AWS_ACCESS_KEY_ID="THAAIOSFODNN7EXAMPLE" + export AWS_SECRET_ACCESS_KEY="LESlrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + export PYTHON_EVALUATOR_URL="http://localhost:8000" + export TOGGLE_GUARDRAILS_ENABLED="false" + + # Run database migrations first + print_status "Running database migrations..." + ./run_db_migrations.sh + + # Start the backend + print_status "Starting backend server..." + java $JAVA_OPTS -jar target/opik-backend-1.0-SNAPSHOT.jar server config.yml & + BACKEND_PID=$! + + # Wait for backend to start + print_status "Waiting for backend to start..." + for i in $(seq 1 30); do + if curl -f http://localhost:8080/health-check >/dev/null 2>&1; then + print_success "Backend is running on http://localhost:8080" + break + fi + if [ $i -eq 30 ]; then + print_error "Backend failed to start after 30 attempts" + kill $BACKEND_PID 2>/dev/null || true + exit 1 + fi + sleep 2 + done +} + +# Function to run frontend locally +run_frontend() { + print_status "Starting frontend locally..." + + frontend_dir="$PROJECT_ROOT/apps/opik-frontend" + + cd "$frontend_dir" + + # Install dependencies if node_modules doesn't exist + if [ ! -d "node_modules" ]; then + print_status "Installing frontend dependencies..." + npm install + fi + + # Ensure .env.development has the correct configuration for local development + print_status "Configuring frontend for local development..." + if [ ! -f ".env.development" ]; then + print_warning ".env.development file not found, creating it..." + fi + + # Update .env.development with correct local development settings + cat > .env.development << EOF +VITE_BASE_URL=/ +VITE_BASE_API_URL=http://localhost:8080 +EOF + + print_success "Frontend environment configured for local development" + + # Start the frontend development server + print_status "Starting frontend development server..." + npm start & + FRONTEND_PID=$! + + # Wait for frontend to start + print_status "Waiting for frontend to start..." + for i in $(seq 1 30); do + if curl -f http://localhost:5174 >/dev/null 2>&1; then + print_success "Frontend is running on http://localhost:5174" + break + fi + if [ $i -eq 30 ]; then + print_error "Frontend failed to start after 30 attempts" + kill $FRONTEND_PID 2>/dev/null || true + exit 1 + fi + sleep 2 + done +} + +# Function to handle cleanup on script exit +cleanup() { + print_status "Cleaning up..." + + # Kill background processes + if [ ! -z "$BACKEND_PID" ]; then + print_status "Stopping backend..." + kill $BACKEND_PID 2>/dev/null || true + fi + + if [ ! -z "$FRONTEND_PID" ]; then + print_status "Stopping frontend..." + kill $FRONTEND_PID 2>/dev/null || true + fi + + print_success "Cleanup completed" +} + +# Function to show usage +show_usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --containers-only Only start containers, don't run backend/frontend locally" + echo " --backend-only Only build and run backend locally" + echo " --frontend-only Only run frontend locally" + echo " --help Show this help message" + echo "" + echo "If no options are provided, the script will:" + echo " 1. Start all required containers (excluding backend & frontend)" + echo " 2. Build the backend with Maven (skipping tests)" + echo " 3. Run backend and frontend locally" +} + +# Set up signal handlers for cleanup +trap cleanup EXIT INT TERM + +# Parse command line arguments +CONTAINERS_ONLY=false +BACKEND_ONLY=false +FRONTEND_ONLY=false + +while [[ $# -gt 0 ]]; do + case $1 in + --containers-only) + CONTAINERS_ONLY=true + shift + ;; + --backend-only) + BACKEND_ONLY=true + shift + ;; + --frontend-only) + FRONTEND_ONLY=true + shift + ;; + --help) + show_usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Main execution +main() { + print_status "Starting OPIK local development environment..." + + # Check requirements + check_requirements + check_docker + + # Start containers + start_containers + + if [ "$CONTAINERS_ONLY" = true ]; then + print_success "Containers started successfully. Use --help for more options." + return + fi + + if [ "$BACKEND_ONLY" = true ]; then + build_backend + run_backend + print_success "Backend is running locally. Press Ctrl+C to stop." + wait $BACKEND_PID + return + fi + + if [ "$FRONTEND_ONLY" = true ]; then + run_frontend + print_success "Frontend is running locally. Press Ctrl+C to stop." + wait $FRONTEND_PID + return + fi + + # Full setup: build and run both backend and frontend + build_backend + run_backend + run_frontend + + print_success "OPIK local development environment is ready!" + print_status "Backend: http://localhost:8080" + print_status "Frontend: http://localhost:5174" + print_status "Press Ctrl+C to stop all services" + + # Wait for user to stop + wait +} + +# Run main function +main "$@" \ No newline at end of file From ab4d3ba1c9149191a836f69ed10312e49aad9633 Mon Sep 17 00:00:00 2001 From: yariv-h Date: Thu, 17 Jul 2025 11:33:41 +0300 Subject: [PATCH 2/2] Adding local dev script for running backend & frontend locally alongside with docker containers for local development --- opik-frontend-cursor-rules.md | 539 ++++++++++++++++++++++++++++++++++ 1 file changed, 539 insertions(+) create mode 100644 opik-frontend-cursor-rules.md diff --git a/opik-frontend-cursor-rules.md b/opik-frontend-cursor-rules.md new file mode 100644 index 0000000000..c08b9acb7a --- /dev/null +++ b/opik-frontend-cursor-rules.md @@ -0,0 +1,539 @@ +# Opik Frontend Development Rules for Cursor + +## Technology Stack & Architecture + +### Core Technologies +- **React 18** with **TypeScript** +- **Vite** as build tool and dev server +- **TanStack Router** for routing +- **TanStack Query** for data fetching and caching +- **Zustand** for state management +- **React Hook Form** with **Zod** for form validation +- **shadcn/ui** + **Radix UI** for UI components +- **Tailwind CSS** for styling with custom design system +- **CodeMirror 6** for code editing/viewing +- **Recharts** for data visualization +- **Lodash** for utility functions + +### Project Structure +``` +src/ +├── api/ # API layer with React Query hooks +├── components/ +│ ├── ui/ # shadcn/ui base components +│ ├── shared/ # Reusable business components +│ ├── layout/ # Layout components +│ ├── pages/ # Page-specific components +│ └── pages-shared/ # Cross-page shared components +├── hooks/ # Custom React hooks +├── lib/ # Utility functions and helpers +├── store/ # Zustand stores +├── types/ # TypeScript type definitions +├── constants/ # Application constants +└── icons/ # SVG icons +``` + +## Component Development Patterns + +### Component Structure +```typescript +// Standard component structure +import React, { useMemo, useCallback } from "react"; +import { cn } from "@/lib/utils"; + +type ComponentProps = { + // Props interface +}; + +const Component: React.FunctionComponent = ({ + prop1, + prop2, + ...props +}) => { + // 1. State hooks + // 2. useMemo for expensive computations + // 3. useCallback for event handlers + // 4. Other hooks + + return ( +
+ {/* JSX */} +
+ ); +}; + +export default Component; +``` + +### Performance Optimization Rules + +#### useMemo Usage +- **Always memoize** data transformations: `const rows = useMemo(() => data?.content ?? [], [data?.content]);` +- **Always memoize** complex computations and filtered/sorted arrays +- **Always memoize** function parameters passed to child components +- **Pattern**: `const processedData = useMemo(() => transformData(rawData), [rawData]);` + +#### useCallback Usage +- **Always wrap** event handlers in useCallback: `const handleClick = useCallback(() => {}, [deps]);` +- **Always wrap** functions passed as props to child components +- **Always wrap** functions used in useEffect dependencies +- **Pattern**: `const deleteHandler = useCallback(() => { /* logic */ }, [dependency]);` + +#### Data Processing +- Use `useMemo` for transforming API responses: `const items = useMemo(() => data?.content ?? [], [data?.content]);` +- Always provide fallback arrays: `data?.content ?? []` +- Process sorting with utility functions: `processSorting(sorting)` + +### UI Component Patterns + +#### Button Variants +Use the established button variant system: +```typescript +// Primary actions + + + +// Secondary actions + + + +// Destructive actions + + +// Minimal/Ghost actions + + + +// Icon buttons + + +``` + +#### Size Variants +```typescript +// Button sizes +size="3xs" | "2xs" | "sm" | "default" | "lg" +size="icon-3xs" | "icon-2xs" | "icon-xs" | "icon-sm" | "icon" | "icon-lg" +``` + +### Data Tables + +#### DataTable Component Pattern +```typescript +const columns: ColumnDef[] = useMemo(() => [ + { + id: COLUMN_ID_ID, + accessorKey: "id", + header: "ID", + size: 100, + meta: { + type: COLUMN_TYPE.string, + }, + }, + // ... more columns +], []); + +const rows = useMemo(() => data?.content ?? [], [data?.content]); + +// Always use DataTable wrapper + +``` + +#### Column Types +Use predefined column types: +- `COLUMN_TYPE.string` - Text data +- `COLUMN_TYPE.number` - Numeric data +- `COLUMN_TYPE.time` - Date/time data +- `COLUMN_TYPE.duration` - Duration data +- `COLUMN_TYPE.cost` - Cost data +- `COLUMN_TYPE.list` - Array data +- `COLUMN_TYPE.dictionary` - Object data +- `COLUMN_TYPE.numberDictionary` - Feedback scores +- `COLUMN_TYPE.category` - Category/tag data + +## Styling Guidelines + +### Design System + +#### Color System +Always use CSS custom properties: +```css +/* Primary colors */ +bg-primary text-primary-foreground +hover:bg-primary-hover active:bg-primary-active + +/* Secondary colors */ +bg-secondary text-secondary-foreground + +/* Muted colors */ +bg-muted text-muted-foreground +text-muted-gray border-muted-disabled + +/* Destructive colors */ +bg-destructive text-destructive-foreground +border-destructive text-destructive + +/* Background variations */ +bg-background bg-primary-foreground bg-popover +``` + +#### Typography Classes +Use custom typography classes: +```css +/* Titles */ +.comet-title-xl /* 3xl font-medium */ +.comet-title-l /* 2xl font-medium */ +.comet-title-m /* xl font-medium */ +.comet-title-s /* lg font-medium */ +.comet-title-xs /* sm font-medium */ + +/* Body text */ +.comet-body /* base font-normal */ +.comet-body-accented /* base font-medium */ +.comet-body-s /* sm font-normal */ +.comet-body-s-accented /* sm font-medium */ +.comet-body-xs /* xs font-normal */ +.comet-body-xs-accented /* xs font-medium */ + +/* Code */ +.comet-code /* monospace font */ +``` + +#### Layout Classes +```css +.comet-header-height /* 64px header */ +.comet-sidebar-width /* sidebar width */ +.comet-content-inset /* content padding */ +.comet-custom-scrollbar /* custom scrollbar */ +.comet-no-scrollbar /* hide scrollbar */ +``` + +#### Spacing and Sizing +- Use consistent spacing: `gap-2`, `gap-4`, `gap-6`, `gap-8` +- Use consistent padding: `p-2`, `p-4`, `p-6`, `px-4`, `py-2` +- Use consistent margins: `mb-4`, `mt-6`, `mx-2` +- Border radius: `rounded-md` (default), `rounded-lg`, `rounded-xl` + +### Component Styling Patterns + +#### Container Patterns +```typescript +// Page containers +
+ +// Card containers +
+ +// Form containers +
+ +// Button groups +
+ +// Grid layouts +
+``` + +#### State Classes +```typescript +// Loading states + + +// Error states +className={cn("border", { "border-destructive": hasError })} + +// Active states +"comet-table-row-active" + +// Disabled states +"disabled:opacity-50 disabled:pointer-events-none" +``` + +## API and Data Fetching + +### React Query Patterns + +#### Query Hook Structure +```typescript +// File: src/api/entity/useEntityList.ts +import { useQuery } from "@tanstack/react-query"; +import api, { ENTITY_KEY, ENTITY_REST_ENDPOINT } from "@/api/api"; + +type UseEntityListParams = { + workspaceName: string; + search?: string; + sorting?: Sorting; + page: number; + size: number; +}; + +const getEntityList = async ( + { signal }: QueryFunctionContext, + params: UseEntityListParams, +) => { + const { data } = await api.get(ENTITY_REST_ENDPOINT, { + signal, + params: { + workspace_name: params.workspaceName, + ...processSorting(params.sorting), + ...(params.search && { name: params.search }), + size: params.size, + page: params.page, + }, + }); + return data; +}; + +export default function useEntityList( + params: UseEntityListParams, + options?: QueryConfig, +) { + return useQuery({ + queryKey: [ENTITY_KEY, params], + queryFn: (context) => getEntityList(context, params), + ...options, + }); +} +``` + +#### Query Key Patterns +- Use descriptive keys: `[ENTITIES_KEY, params]` +- Include all parameters that affect the query +- Use constants for query keys defined in `api.ts` + +#### Data Processing +```typescript +// Always provide fallbacks and memoize +const entities = useMemo(() => data?.content ?? [], [data?.content]); +const totalItems = data?.total ?? 0; +``` + +### Mutation Patterns +```typescript +const mutation = useMutation({ + mutationFn: deleteEntity, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [ENTITIES_KEY] }); + toast({ description: "Entity deleted successfully" }); + }, + onError: (error) => { + toast({ description: "Failed to delete entity", variant: "destructive" }); + }, +}); +``` + +## State Management + +### Zustand Store Pattern +```typescript +// File: src/store/EntityStore.ts +import { create } from "zustand"; + +type EntityState = { + selectedEntity: Entity | null; + filters: FilterState; +}; + +type EntityActions = { + setSelectedEntity: (entity: Entity | null) => void; + updateFilters: (filters: Partial) => void; + resetFilters: () => void; +}; + +type EntityStore = EntityState & EntityActions; + +const useEntityStore = create((set) => ({ + // State + selectedEntity: null, + filters: defaultFilters, + + // Actions + setSelectedEntity: (entity) => set({ selectedEntity: entity }), + updateFilters: (newFilters) => + set((state) => ({ filters: { ...state.filters, ...newFilters } })), + resetFilters: () => set({ filters: defaultFilters }), +})); + +// Exported selectors +export const useSelectedEntity = () => + useEntityStore((state) => state.selectedEntity); +export const useEntityFilters = () => + useEntityStore((state) => state.filters); + +export default useEntityStore; +``` + +### Local Storage Integration +```typescript +// Use use-local-storage-state for persistence +import useLocalStorageState from "use-local-storage-state"; + +const [preferences, setPreferences] = useLocalStorageState("key", { + defaultValue: defaultPreferences, +}); +``` + +## Form Handling + +### React Hook Form + Zod Pattern +```typescript +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +// Define schema +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email"), +}); + +type FormData = z.infer; + +// Use in component +const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + email: "", + }, +}); + +const onSubmit = useCallback((data: FormData) => { + // Handle form submission +}, []); + +// Form JSX +
+ + ( + + Name + + + + + + )} + /> + + +``` + +## Error Handling and Loading States + +### Loading Patterns +```typescript +// Query loading +if (isLoading) return ; +if (error) return
Error: {error.message}
; + +// Component loading +{isLoading && } + +// Button loading + +``` + +### Toast Notifications +```typescript +import { useToast } from "@/components/ui/use-toast"; + +const { toast } = useToast(); + +// Success toast +toast({ description: "Operation completed successfully" }); + +// Error toast +toast({ + description: "Operation failed", + variant: "destructive" +}); +``` + +## Code Quality Rules + +### TypeScript Patterns +- **Always** define explicit prop interfaces +- **Always** use `React.FunctionComponent` for components +- **Use** strict type checking for API responses +- **Prefer** type unions over enums where appropriate +- **Always** provide return types for complex functions + +### Import Organization +```typescript +// 1. React and external libraries +import React, { useMemo, useCallback } from "react"; +import { useQuery } from "@tanstack/react-query"; + +// 2. UI components (grouped) +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; + +// 3. Shared components +import DataTable from "@/components/shared/DataTable/DataTable"; + +// 4. Hooks and utilities +import { cn } from "@/lib/utils"; +import useEntityStore from "@/store/EntityStore"; + +// 5. Types and constants +import { COLUMN_TYPE } from "@/types/shared"; +``` + +### Naming Conventions +- **Components**: PascalCase (`DataTable`, `UserProfile`) +- **Files**: PascalCase for components, camelCase for utilities +- **Hooks**: camelCase starting with `use` (`useEntityList`) +- **Constants**: SCREAMING_SNAKE_CASE (`COLUMN_TYPE`, `API_ENDPOINTS`) +- **CSS Classes**: Use `comet-` prefix for custom classes +- **Event Handlers**: Descriptive names (`handleDeleteClick`, `onEntitySelect`) + +### Performance Best Practices +- **Always** memoize data transformations with `useMemo` +- **Always** memoize event handlers with `useCallback` +- **Always** provide dependency arrays for hooks +- **Use** lazy loading for large components/routes +- **Avoid** inline functions in JSX props +- **Prefer** pagination over infinite scrolling for large datasets + +### Component Composition +- **Favor** composition over prop drilling +- **Use** context sparingly, prefer prop passing +- **Create** reusable compound components (Dialog, Card, etc.) +- **Extract** common logic into custom hooks +- **Keep** components focused and single-responsibility + +### Code Formatting +- **Use** Prettier with provided configuration +- **Follow** ESLint rules (no warnings allowed) +- **Use** trailing commas in multiline structures +- **Prefer** double quotes for strings +- **Use** meaningful variable names + +## Accessibility Guidelines +- **Always** provide `aria-label` for icon buttons +- **Use** semantic HTML elements +- **Ensure** keyboard navigation works +- **Provide** focus indicators +- **Use** proper heading hierarchy +- **Include** loading states and error messages + +## Testing Patterns +- **Write** tests for custom hooks using `@testing-library/react` +- **Test** user interactions, not implementation details +- **Mock** API calls using MSW or similar +- **Use** `screen.getByRole` over `getByTestId` +- **Write** integration tests for complex user flows + +When building new components or features, always follow these established patterns to ensure consistency with the existing codebase. The senior developer who created this project prioritized type safety, performance optimization, accessibility, and maintainable code architecture. \ No newline at end of file