diff --git a/llms.txt b/llms.txt deleted file mode 100644 index b648413..0000000 --- a/llms.txt +++ /dev/null @@ -1,1231 +0,0 @@ -# llms.txt — LLM Guide for Building Great Databricks Apps with AppKit -Project: Databricks AppKit - -This document is written *for LLMs* generating code in a brand-new project folder that installs AppKit from npm. It is intentionally prescriptive. - -## High-level mission - -Build **full-stack TypeScript apps** on Databricks using: - -- **Backend**: `@databricks/appkit` -- **Frontend**: `@databricks/appkit-ui` -- **Analytics**: SQL files in `config/queries/*.sql` executed via the AppKit analytics plugin - -This file is designed to work even when you *do not* have access to the AppKit source repo. Prefer only public package APIs and portable project structures. - -## Hard rules (LLM guardrails) - -- **Do not invent APIs**. If unsure, stick to the patterns shown in this file and only documented exports from `@databricks/appkit` and `@databricks/appkit-ui`. -- **`createApp()` is async**. Prefer **top-level `await createApp(...)`**. If you can’t, use `void createApp(...)` and do not ignore promise rejection. -- **Always memoize query parameters** passed to `useAnalyticsQuery` / charts to avoid refetch loops. -- **Always handle loading/error/empty states** in UI (use `Skeleton`, error text, empty state). -- **Always use `sql.*` helpers** for query parameters (do not pass raw strings/numbers unless the query expects none). -- **Never construct SQL strings dynamically**. Use parameterized queries with `:paramName`. -- **Never use `require()`**. Use ESM `import/export`. - -## TypeScript import rules (when using `verbatimModuleSyntax`) - -If your `tsconfig.json` uses `"verbatimModuleSyntax": true`, **always use `import type` for type-only imports** (otherwise builds can fail in strict setups): - -```ts -import type { ReactNode } from "react"; -import { useMemo } from "react"; -``` - -## Canonical project layout - -Recommended structure (client/server split): - -``` -my-app/ -├── server/ -│ ├── index.ts # backend entry point (AppKit) -│ └── .env # optional local dev env vars (do not commit) -├── client/ -│ ├── index.html -│ ├── vite.config.ts -│ └── src/ -│ ├── main.tsx -│ └── App.tsx -├── config/ -│ └── queries/ -│ └── my_query.sql -├── app.yaml -├── package.json -└── tsconfig.json -``` - -Why this layout: - -- The AppKit `server()` plugin automatically serves: - - **Dev**: Vite dev server (HMR) from `client/` - - **Prod**: static files from `client/dist` (built by Vite) - -## Project scaffolding (start here) - -### `package.json` - -```json -{ - "name": "my-app", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "NODE_ENV=development tsx watch server/index.ts", - "build": "npm run build:server && npm run build:client", - "build:server": "tsdown --out-dir build server/index.ts", - "build:client": "tsc -b && vite build --config client/vite.config.ts", - "start": "node build/index.mjs" - }, - "dependencies": { - "@databricks/appkit": "^0.1.2" - "@databricks/appkit-ui": "^0.1.2", - "react": "^19.2.3", - "react-dom": "^19.2.3" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react": "^5.1.1", - "tsdown": "^0.15.7", - "tsx": "^4.19.0", - "typescript": "~5.6.0", - "vite": "^7.2.4" - } -} -``` - -### `client/index.html` - -```html - - - - - - My App - - -
- - - -``` - -### `client/src/main.tsx` - -```tsx -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import App from "./App"; - -createRoot(document.getElementById("root")!).render( - - - , -); -``` - -### `client/src/App.tsx` (minimal) - -```tsx -export default function App() { - return ( -
-

My App

-
- ); -} -``` - -### `client/vite.config.ts` - -```ts -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], -}); -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true - }, - "include": ["server", "client/src"] -} -``` - -### `server/index.ts` - -```ts -import { createApp, server } from "@databricks/appkit"; - -await createApp({ - plugins: [server()], -}); -``` - -### Running the app - -```bash -# Install dependencies -npm install - -# Development (starts backend + Vite dev server) -npm run dev - -# Production build -npm run build -npm start -``` - -## Integrating into an existing app - -If you already have a React/Vite app and want to add AppKit: - -### 1. Install dependencies - -```bash -npm install @databricks/appkit @databricks/appkit-ui react react-dom -npm install -D tsx tsdown vite @vitejs/plugin-react typescript - -# If you don't already have a client/ folder, create one and move your Vite app into it: -# - move index.html -> client/index.html -# - move vite.config.ts -> client/vite.config.ts -# - move src/ -> client/src/ -# -``` - -### 2. Create `server/index.ts` (new file) - -```ts -import { createApp, server } from "@databricks/appkit"; - -await createApp({ - plugins: [server()], -}); -``` - -### 3. Update `package.json` scripts - -```json -{ - "scripts": { - "dev": "NODE_ENV=development tsx watch server/index.ts", - "build": "npm run build:server && npm run build:client", - "build:server": "tsdown --out-dir build server/index.ts", - "build:client": "tsc -b && vite build --config client/vite.config.ts", - "start": "node build/index.mjs" - } -} -``` - -### 4. That's it - -- AppKit's server plugin will automatically serve your Vite app in dev mode and `client/dist` in production. -- If your Vite app must stay at the repo root (no `client/` folder), AppKit can still work, but the recommended layout is `client/` + `server/`. - -### Adding analytics to an existing app - -```ts -// server/index.ts -import { createApp, server, analytics } from "@databricks/appkit"; - -await createApp({ - plugins: [server(), analytics()], -}); -``` - -Then create `config/queries/` and add your `.sql` files. - -## Environment variables - -### Required for Databricks Apps deployment - -These are typically **provided by Databricks Apps runtime** (exact set can vary by platform/version): - -| Variable | Description | -|----------|-------------| -| `DATABRICKS_HOST` | Workspace URL (e.g. `https://xxx.cloud.databricks.com`) | -| `DATABRICKS_APP_PORT` | Port to bind (default: `8000`) | -| `DATABRICKS_APP_NAME` | App name in Databricks | - -### Required for SQL queries (analytics plugin) - -| Variable | Description | How to set | -|----------|-------------|------------| -| `DATABRICKS_WAREHOUSE_ID` | SQL warehouse ID | In `app.yaml`: `valueFrom: sql-warehouse` | - -### Optional - -| Variable | Description | Default | -|----------|-------------|---------| -| `DATABRICKS_WORKSPACE_ID` | Workspace ID | Auto-fetched from API | -| `NODE_ENV` | `"development"` or `"production"` | — | -| `FLASK_RUN_HOST` | Host to bind | `0.0.0.0` | - -### Local development - -For local development, you need to authenticate with Databricks. Options: - -**Option 1: Databricks CLI Auth (recommended)** - -```bash -# Configure once -databricks auth login --host [host] --profile [profile-name] - -# If you used `DEFAULT` as the profile name then you can just run - -`npm run dev` - -# To run with a specific profile -DATABRICKS_CONFIG_PROFILE=my-profile npm run dev -# If your Databricks SDK expects a different variable name, try: -# DATABRICKS_PROFILE=my-profile npm run dev -``` - -**Option 2: Environment variables** - -```bash -export DATABRICKS_HOST="https://xxx.cloud.databricks.com" -export DATABRICKS_TOKEN="dapi..." -export DATABRICKS_WAREHOUSE_ID="abc123..." -npm run dev -``` - -**Option 3: `.env` file (auto-loaded by AppKit)** - -```bash -# .env (add to .gitignore!) -DATABRICKS_HOST=https://xxx.cloud.databricks.com -DATABRICKS_TOKEN=dapi... -DATABRICKS_WAREHOUSE_ID=abc123... -``` - -### Telemetry (optional) - -| Variable | Description | -|----------|-------------| -| `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry collector endpoint | -| `OTEL_SERVICE_NAME` | Service name for traces | - -## Backend: `@databricks/appkit` - -### Minimal server (golden template) - -The smallest valid AppKit server: - -```ts -// server/index.ts -import { createApp, server } from "@databricks/appkit"; - -await createApp({ - plugins: [server()], -}); -``` - -### Server plugin (`server()`) - -What it does: - -- Starts an Express server (default `host=0.0.0.0`, `port=8000`) -- Mounts plugin routes under `/api//...` -- Adds `/health` (returns `{ status: "ok" }`) -- Serves frontend: - - **Development** (`NODE_ENV=development`): runs a Vite dev server in middleware mode - - **Production**: auto-detects static frontend directory (checks `dist`, `client/dist`, `build`, `public`, `out`) - -Config (real options): - -```ts -import { createApp, server } from "@databricks/appkit"; - -await createApp({ - plugins: [ - server({ - port: 8000, // default: Number(process.env.DATABRICKS_APP_PORT) || 8000 - host: "0.0.0.0", // default: process.env.FLASK_RUN_HOST || "0.0.0.0" - autoStart: true, // default: true - staticPath: "dist", // optional: force a specific static directory - }), - ], -}); -``` - -Manual server start (when you need to `.extend()` Express): - -```ts -import { createApp, server } from "@databricks/appkit"; - -const appkit = await createApp({ - plugins: [server({ autoStart: false })], -}); - -appkit.server.extend((app) => { - app.get("/custom", (_req, res) => res.json({ ok: true })); -}); - -await appkit.server.start(); -``` - -### Analytics plugin (`analytics()`) - -Add SQL query execution backed by Databricks SQL Warehouses. - -```ts -import { analytics, createApp, server } from "@databricks/appkit"; - -await createApp({ - plugins: [server(), analytics({})], -}); -``` - -Where queries live: - -- Put `.sql` files in `config/queries/`. -- Query key is the filename without `.sql` (e.g. `spend_summary.sql` → `"spend_summary"`). - -SQL parameters: - -- Use `:paramName` placeholders. -- Optionally annotate parameter types using SQL comments: - -```sql --- @param startDate DATE --- @param endDate DATE --- @param limit NUMERIC -SELECT ... -WHERE usage_date BETWEEN :startDate AND :endDate -LIMIT :limit -``` - -Supported `-- @param` types (case-insensitive): - -- `STRING`, `NUMERIC`, `BOOLEAN`, `DATE`, `TIMESTAMP`, `BINARY` - -Server-injected params (important): - -- `:workspaceId` is **injected by the server** and **must not** be annotated. -- Example: - -```sql -WHERE workspace_id = :workspaceId -``` - -HTTP endpoints exposed (mounted under `/api/analytics`): - -- `POST /api/analytics/query/:query_key` -- `POST /api/analytics/users/me/query/:query_key` -- `GET /api/analytics/arrow-result/:jobId` -- `GET /api/analytics/users/me/arrow-result/:jobId` - -Formats: - -- `format: "JSON"` (default) returns JSON rows -- `format: "ARROW"` returns an Arrow “statement_id” payload over SSE, then the client fetches binary Arrow from `/api/analytics/arrow-result/:jobId` - -### Execution context and `asUser(req)` - -AppKit manages Databricks authentication via two contexts: - -- **ServiceContext** (singleton): Initialized at app startup with service principal credentials -- **ExecutionContext**: Determined at runtime - either service principal or user context - -**Headers used for user context:** - -- `x-forwarded-user`: required in production; identifies the user -- `x-forwarded-access-token`: required for user token passthrough - -**Using `asUser(req)` for user-scoped operations:** - -The `asUser(req)` pattern allows plugins to execute operations using the requesting user's credentials: - -```ts -// In a custom plugin route handler -router.post("/users/me/data", async (req, res) => { - // Execute as the user (uses their Databricks permissions) - const result = await this.asUser(req).query("SELECT ..."); - res.json(result); -}); - -// Service principal execution (default) -router.post("/system/data", async (req, res) => { - const result = await this.query("SELECT ..."); - res.json(result); -}); -``` - -**Context helper functions (exported from `@databricks/appkit`):** - -- `getExecutionContext()`: Returns current context (user or service) -- `getCurrentUserId()`: Returns user ID in user context, service user ID otherwise -- `getWorkspaceClient()`: Returns the appropriate WorkspaceClient for current context -- `getWarehouseId()`: `Promise` (from `DATABRICKS_WAREHOUSE_ID` or auto-selected in dev) -- `getWorkspaceId()`: `Promise` (from `DATABRICKS_WORKSPACE_ID` or fetched) -- `isInUserContext()`: Returns `true` if currently executing in user context - -**Development mode behavior:** - -In local development (`NODE_ENV=development`), if `asUser(req)` is called without a user token, it logs a warning and falls back to the service principal. - -### Custom plugins (backend) - -If you need custom API routes or background logic, implement an AppKit plugin. - -```ts -import { Plugin, toPlugin } from "@databricks/appkit"; -import type express from "express"; - -class MyPlugin extends Plugin { - name = "my-plugin"; - envVars = []; // list required env vars here - - injectRoutes(router: express.Router) { - this.route(router, { - name: "hello", - method: "get", - path: "/hello", - handler: async (_req, res) => { - res.json({ ok: true }); - }, - }); - } -} - -export const myPlugin = toPlugin, "my-plugin">( - MyPlugin, - "my-plugin", -); -``` - -### Caching (global + plugin-level) - -Global: - -```ts -await createApp({ - plugins: [server(), analytics({})], - cache: { - enabled: true, - ttl: 3600, // seconds - strictPersistence: false, - }, -}); -``` - -- Storage auto-selects **Lakebase persistent cache when healthy**, otherwise falls back to in-memory. - -Plugin-level: - -```ts -// inside a Plugin subclass: -const value = await this.cache.getOrExecute( - ["my-plugin", "data", userId], - async () => expensiveWork(), - userKey, - { ttl: 300 }, -); -``` - -## Frontend: `@databricks/appkit-ui` - -### Imports - -- React-facing APIs: `@databricks/appkit-ui/react` -- Non-React utilities (sql markers, arrow, SSE): `@databricks/appkit-ui/js` - -```tsx -import { useAnalyticsQuery, Card, Skeleton } from "@databricks/appkit-ui/react"; -import { sql } from "@databricks/appkit-ui/js"; -``` - -### `useAnalyticsQuery(queryKey, parameters, options?)` - -Facts: - -- Uses **SSE** under the hood (not `fetch()` polling). -- By default it hits `POST /api/analytics/query/:queryKey`. -- Returns `{ data, loading, error }` where `data` is `null` until loaded. -- `format` is `"JSON"` or `"ARROW"` (uppercase). - -When to use it: - -- Use `useAnalyticsQuery` **only** when you need a custom UI (cards/KPIs/forms/conditional rendering). -- If you just need a standard chart or table, prefer the built-in components (`BarChart`, `LineChart`, `DataTable`, etc.) so you don’t re-implement loading/error/empty states. - -Limitations (common LLM pitfall): - -- There is **no `enabled` option**. Use conditional rendering to mount/unmount the component. -- There is **no `refetch()`**. Change `parameters` (memoized) or re-mount to re-run the query. - -Recommended usage pattern (memoized params + explicit states): - -```tsx -import { useMemo } from "react"; -import { useAnalyticsQuery, Skeleton } from "@databricks/appkit-ui/react"; -import { sql } from "@databricks/appkit-ui/js"; - -export function Users() { - const params = useMemo( - () => ({ - status: sql.string("active"), - limit: sql.number(50), - }), - [], - ); - - const { data, loading, error } = useAnalyticsQuery("users_list", params); - - if (loading) return ; - if (error) return
Error: {error}
; - if (!data || data.length === 0) return
No results
; - - return
{JSON.stringify(data[0], null, 2)}
; -} -``` - -Options: - -- `format?: "JSON" | "ARROW"` (default `"JSON"`) -- `autoStart?: boolean` (default `true`) -- `maxParametersSize?: number` (default `100 * 1024` bytes) - -### `useChartData({ queryKey, parameters, format, transformer })` - -- `format` here is **lowercase**: `"json" | "arrow" | "auto"` (default `"auto"`) -- Auto-selection heuristics: - - If `parameters._preferArrow === true` → Arrow - - If `parameters._preferJson === true` → JSON - - If `parameters.limit` is a number > 500 → Arrow - - If `parameters.startDate` and `parameters.endDate` exist → Arrow - -### Charts (unified query/data API) - -All charts support: - -- **Query mode**: `queryKey` + `parameters` -- **Data mode**: `data` (inline JSON, no server) - -Available chart components: - -- `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `DonutChart`, `HeatmapChart`, `ScatterChart`, `RadarChart` - -Avoid double-fetching: - -```tsx -// ❌ Wrong: fetches the same query twice -// const { data } = useAnalyticsQuery("spend_data", params); -// return ; - -// ✅ Correct: let the chart fetch -return ; -``` - -Query mode (recommended for Databricks-backed analytics): - -```tsx -import { LineChart } from "@databricks/appkit-ui/react"; -import { sql } from "@databricks/appkit-ui/js"; -import { useMemo } from "react"; - -export function SpendChart() { - const params = useMemo( - () => ({ - startDate: sql.date("2024-01-01"), - endDate: sql.date("2024-12-31"), - aggregationLevel: sql.string("day"), - }), - [], - ); - - return ( - - ); -} -``` - -**Chart props reference (important):** - -Charts are **self-contained ECharts components**. Configure via props, NOT children: - -```tsx -// ✅ Correct: use props for customization - - - -``` - -**❌ CRITICAL: Charts do NOT accept Recharts children** - -```tsx -// ❌ WRONG - AppKit charts are NOT Recharts wrappers -import { BarChart } from "@databricks/appkit-ui/react"; -import { Bar, XAxis, YAxis, CartesianGrid } from "recharts"; - - - // ❌ This will cause TypeScript errors - // ❌ Not supported - // ❌ Not supported - - -// ✅ CORRECT - use props instead - -``` - -### SQL helpers (`sql.*`) - -Use these to build typed parameters (they return marker objects: `{ __sql_type, value }`): - -- `sql.string(value)` → STRING (accepts string|number|boolean) -- `sql.number(value)` → NUMERIC (accepts number|string) -- `sql.boolean(value)` → BOOLEAN (accepts boolean|string("true"/"false")|number(1/0)) -- `sql.date(value)` → DATE (accepts Date or `"YYYY-MM-DD"`) -- `sql.timestamp(value)` → TIMESTAMP (accepts Date, ISO string, or unix time) - -Binary parameters (important): - -- Databricks SQL Warehouse doesn't support `BINARY` as a parameter type. -- `sql.binary(value)` returns a **STRING marker containing hex**, so use `UNHEX(:param)` in SQL. -- `sql.binary` accepts `Uint8Array`, `ArrayBuffer`, or a hex string. - -### SQL result types (important) - -Databricks SQL JSON results can return some numeric-like fields (especially `DECIMAL`) as strings. If a field behaves like a string at runtime, convert explicitly: - -```ts -const value = Number(row.amount); -``` - -If you need more reliable numeric fidelity for large datasets, prefer `format: "ARROW"` and process Arrow on the client. - -### `connectSSE` (custom SSE connections) - -For custom streaming endpoints (not analytics), use the `connectSSE` utility: - -```tsx -import { connectSSE } from "@databricks/appkit-ui/js"; -import { useEffect, useState } from "react"; - -function useCustomStream(endpoint: string) { - const [messages, setMessages] = useState([]); - const [connected, setConnected] = useState(false); - - useEffect(() => { - const controller = new AbortController(); - - connectSSE({ - url: endpoint, - payload: { key: "value" }, // optional: makes it a POST - onMessage: async ({ data }) => { - setConnected(true); - setMessages((prev) => [...prev, data]); - }, - onError: (error) => { - console.error("SSE error:", error); - setConnected(false); - }, - signal: controller.signal, - maxRetries: 3, // default: 3 - retryDelay: 2000, // default: 2000ms (exponential backoff) - timeout: 300000, // default: 5 minutes - maxBufferSize: 1048576, // default: 1MB - }); - - return () => controller.abort(); - }, [endpoint]); - - return { messages, connected }; -} -``` - -Options: - -- `url`: SSE endpoint URL (required) -- `payload`: Optional request body (if provided, uses POST; otherwise GET) -- `onMessage({ id, data })`: Called for each SSE message -- `onError(error)`: Called on connection errors -- `signal`: AbortSignal to cancel the connection -- `lastEventId`: Resume from a specific event ID -- `maxRetries`: Max retry attempts (default: 3) -- `retryDelay`: Base delay between retries in ms (default: 2000) -- `timeout`: Connection timeout in ms (default: 300000) -- `maxBufferSize`: Max buffer size in bytes (default: 1MB) - -### `ArrowClient` (advanced Arrow processing) - -For low-level Arrow data handling: - -```tsx -import { ArrowClient } from "@databricks/appkit-ui/js"; - -// Process Arrow buffer -const table = await ArrowClient.processArrowBuffer(buffer); - -// Fetch and process Arrow data in one call -const table = await ArrowClient.fetchAndProcessArrow(url, headers); - -// Extract fields from table -const fields = ArrowClient.extractArrowFields(table); -// → [{ name: "date", type: ... }, { name: "value", type: ... }] - -// Extract columns as arrays -const columns = ArrowClient.extractArrowColumns(table); -// → { date: [...], value: [...] } - -// Extract chart data -const { xData, yDataMap } = ArrowClient.extractChartData(table, "date", ["value", "count"]); -// → { xData: [...], yDataMap: { value: [...], count: [...] } } - -// Auto-detect chart fields from Arrow table -const detected = ArrowClient.detectFieldsFromArrow(table); -// → { xField: "date", yFields: ["value"], chartType: "timeseries" } -``` - -### DataTable - -`DataTable` is a production-ready table integrated with `useAnalyticsQuery`. - -Key behaviors: - -- `parameters` is required (use `{}` if none) -- Supports opinionated mode (auto columns) and full-control mode (`children(table)`) - -```tsx -import { DataTable } from "@databricks/appkit-ui/react"; - -export function UsersTable() { - return ( - - ); -} -``` - -### UI components (primitives) - -AppKit-UI ships shadcn-style primitives. Import from `@databricks/appkit-ui/react`. - -Note: Exact exports can vary by AppKit-UI version. Prefer using IDE auto-import/autocomplete to confirm what your installed version exports. - -Radix constraint (common bug): - -- `SelectItem` cannot have `value=""`. Use a sentinel value like `"all"` or `"none"`. - -**Available components:** - -`Accordion`, `Alert`, `AlertDialog`, `AspectRatio`, `Avatar`, `Badge`, `Breadcrumb`, `Button`, `ButtonGroup`, `Calendar`, `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `Carousel`, `Checkbox`, `Collapsible`, `Command`, `ContextMenu`, `Dialog`, `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogFooter`, `Drawer`, `DropdownMenu`, `Empty`, `Field`, `Form`, `HoverCard`, `Input`, `InputGroup`, `InputOtp`, `Item`, `Kbd`, `Label`, `Menubar`, `NavigationMenu`, `Pagination`, `Popover`, `Progress`, `RadioGroup`, `Resizable`, `ScrollArea`, `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `Separator`, `Sheet`, `Sidebar`, `Skeleton`, `Slider`, `Sonner`, `Spinner`, `Switch`, `Table`, `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `Textarea`, `Toggle`, `ToggleGroup`, `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider` - -### Card pattern - -```tsx -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardFooter, -} from "@databricks/appkit-ui/react"; - -function MetricCard({ title, value, description }: Props) { - return ( - - - {description} - {value} - - - {/* Optional content */} - - - {/* Optional footer */} - - - ); -} -``` - -### Select pattern - -```tsx -import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, -} from "@databricks/appkit-ui/react"; - -function DateRangeSelect({ value, onChange }: Props) { - return ( - - ); -} -``` - -### Tabs pattern - -```tsx -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@databricks/appkit-ui/react"; - -function Dashboard() { - return ( - - - Overview - Analytics - - -

Overview content

-
- -

Analytics content

-
-
- ); -} -``` - -### Dialog pattern - -```tsx -import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, - Button, -} from "@databricks/appkit-ui/react"; - -function ConfirmDialog() { - return ( - - - - - - - Confirm deletion - - This action cannot be undone. - - - - - - - - - ); -} -``` - -### TooltipProvider requirement - -If using tooltips anywhere in your app, wrap your root component with `TooltipProvider`: - -```tsx -import { TooltipProvider } from "@databricks/appkit-ui/react"; - -function App() { - return ( - - {/* Your app content */} - - ); -} -``` - -### Button variants - -```tsx -import { Button } from "@databricks/appkit-ui/react"; - - - - - - - -``` - -### Loading skeleton pattern - -```tsx -import { Card, CardHeader, Skeleton } from "@databricks/appkit-ui/react"; - -function LoadingCard() { - return ( - - - - - - - - ); -} -``` - -## Stylesheet - -In the main css file import the following - -```css -@import "@databricks/appkit-ui/styles.css"; -``` - -That will provide a default theme for the app using css variables. - -### Customizing theme (light/dark mode) - -- Full list of variables to customize the theme. - -```css -@import "@databricks/appkit-ui/styles.css"; - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.141 0.005 285.823); - --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.005 285.823); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.21 0.006 285.885); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); - --accent: oklch(0.967 0.001 286.375); - --accent-foreground: oklch(0.21 0.006 285.885); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.985 0 0); - --success: oklch(0.603 0.135 166.892); - --success-foreground: oklch(1 0 0); - --warning: oklch(0.795 0.157 78.748); - --warning-foreground: oklch(0.199 0.027 238.732); - --border: oklch(0.92 0.004 286.32); - --input: oklch(0.92 0.004 286.32); - --ring: oklch(0.705 0.015 286.067); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.141 0.005 285.823); - --sidebar-primary: oklch(0.21 0.006 285.885); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.967 0.001 286.375); - --sidebar-accent-foreground: oklch(0.21 0.006 285.885); - --sidebar-border: oklch(0.92 0.004 286.32); - --sidebar-ring: oklch(0.705 0.015 286.067); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: oklch(0.141 0.005 285.823); - --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0.006 285.885); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0.006 285.885); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.92 0.004 286.32); - --primary-foreground: oklch(0.21 0.006 285.885); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.274 0.006 286.033); - --muted-foreground: oklch(0.705 0.015 286.067); - --accent: oklch(0.274 0.006 286.033); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(0.985 0 0); - --success: oklch(0.67 0.12 167); - --success-foreground: oklch(1 0 0); - --warning: oklch(0.83 0.165 85); - --warning-foreground: oklch(0.199 0.027 238.732); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.552 0.016 285.938); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.21 0.006 285.885); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.274 0.006 286.033); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.552 0.016 285.938); - } -} - -``` - -- If any variable is changed, it must be changed for both light and dark mode. - -## Type generation (QueryRegistry + IntelliSense) - -Goal: generate `client/src/appKitTypes.d.ts` so query keys, params, and result rows are type-safe. - -### Vite plugin: `appKitTypesPlugin` - -Correct option names: - -- `outFile?: string` (default `src/appKitTypes.d.ts`) -- `watchFolders?: string[]` (default `["../config/queries"]`) - -```ts -// client/vite.config.ts -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import { appKitTypesPlugin } from "@databricks/appkit"; - -export default defineConfig({ - plugins: [ - react(), - appKitTypesPlugin({ - outFile: "src/appKitTypes.d.ts", - watchFolders: ["../config/queries"], - }), - ], -}); -``` - -Important nuance: - -- When the frontend is served through AppKit in dev mode, AppKit’s dev server already includes `appKitTypesPlugin()` internally. -- You still want it in your client build pipeline if you run `vite build` separately. - -### CLI: `appkit-generate-types` - -```bash -# Requires DATABRICKS_WAREHOUSE_ID (or pass as 3rd arg) -npx appkit-generate-types [rootDir] [outFile] [warehouseId] - -# Example: -npx appkit-generate-types . client/src/appKitTypes.d.ts - -# Force regeneration (skip cache): -npx appkit-generate-types --no-cache -``` - -## Databricks Apps config: `app.yaml` - -Bind a SQL warehouse for Apps runtime: - -```yaml -env: - - name: DATABRICKS_WAREHOUSE_ID - valueFrom: sql-warehouse -``` - -Full example with command: - -```yaml -command: - - node - - build/index.mjs -env: - - name: DATABRICKS_WAREHOUSE_ID - valueFrom: sql-warehouse -``` - -## LLM checklist (before you "finalize" code) - -- **Project setup** - - `package.json` has `"type": "module"` - - `tsx` is in devDependencies for dev server - - `dev` script uses `NODE_ENV=development tsx watch server/index.ts` - - `client/index.html` exists with `
` and script pointing to `client/src/main.tsx` - -- **Backend** - - `await createApp({ plugins: [...] })` is used (or `void createApp` with intent) - - `server()` is included (always) - - If using SQL: `analytics({})` included + `config/queries/*.sql` present - - Queries use `:param` placeholders, and params are passed from UI using `sql.*` - - If query needs workspace scoping: uses `:workspaceId` - -- **Frontend** - - `useMemo` wraps parameters objects - - Loading/error/empty states are explicit - - Charts use `format="auto"` unless you have a reason to force `"json"`/`"arrow"` - - Charts use props (`xKey`, `yKey`, `colors`) NOT children (they're ECharts-based, not Recharts) - - If using tooltips: root is wrapped with `` - -- **Never** - - Don't build SQL strings manually - - Don't pass untyped raw params for annotated queries - - Don't ignore `createApp()`'s promise - - Don't invent UI components not listed in this file - - Don't pass Recharts children (``, ``, etc.) to AppKit chart components -