diff --git a/experimental/aitools/lib/prompts/target_apps.tmpl b/experimental/aitools/lib/prompts/target_apps.tmpl index ec89e93e54..78f29af319 100644 --- a/experimental/aitools/lib/prompts/target_apps.tmpl +++ b/experimental/aitools/lib/prompts/target_apps.tmpl @@ -2,7 +2,8 @@ * L2: Target-specific guidance for Databricks Apps. * * Injected when: target type "apps" is detected or after init-template. - * Contains: app naming constraints, validation, deployment consent. + * Contains: validation, deployment consent, operational commands. + * AppKit-specific guidance is in skills/apps/appkit/ - load via read_skill_file. */ -}} ## Databricks Apps Development diff --git a/experimental/aitools/lib/skills/apps/.gitkeep b/experimental/aitools/lib/skills/apps/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/experimental/aitools/lib/skills/apps/appkit/SKILL.md b/experimental/aitools/lib/skills/apps/appkit/SKILL.md new file mode 100644 index 0000000000..38f20d71a8 --- /dev/null +++ b/experimental/aitools/lib/skills/apps/appkit/SKILL.md @@ -0,0 +1,139 @@ +--- +name: appkit +description: Build full-stack TypeScript Databricks Apps using @databricks/appkit (backend) and @databricks/appkit-ui (frontend). Use for analytics dashboards, data apps, SQL query visualization, SSE streaming, and Arrow data processing. +--- + +# AppKit - Full-Stack TypeScript Databricks Apps + +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 + +## Hard Rules (LLM Guardrails) + +- **Do not invent APIs**. Stick to patterns shown here and documented exports only. +- **`createApp()` is async**. Use **top-level `await createApp(...)`**. +- **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). +- **Never construct SQL strings dynamically**. Use parameterized queries with `:paramName`. +- **Never use `require()`**. Use ESM `import/export`. +- **Charts do NOT accept children**. Use props (`xKey`, `yKey`, `colors`) NOT Recharts children. + +## TypeScript Import Rules + +With `verbatimModuleSyntax: true`, **always use `import type` for type-only imports**: + +```ts +import type { ReactNode } from "react"; +import { useMemo } from "react"; +``` + +## Detailed Documentation + +- **Backend**: [appkit-backend.md](appkit-backend.md) +- **Frontend**: [appkit-frontend.md](appkit-frontend.md) +- **Project Setup**: [appkit-scaffolding.md](appkit-scaffolding.md) + +## Quick Start + +### Minimal Server + +```ts +// server/index.ts +import { createApp, server, analytics } from "@databricks/appkit"; + +await createApp({ + plugins: [server(), analytics({})], +}); +``` + +### Minimal Frontend + +```tsx +// client/src/App.tsx +import { BarChart, Card, CardHeader, CardTitle, CardContent } from "@databricks/appkit-ui/react"; +import { useMemo } from "react"; + +export default function App() { + const params = useMemo(() => ({}), []); + + return ( + + + Sales by Region + + + + + + ); +} +``` + +### SQL Query + +```sql +-- config/queries/sales_by_region.sql +SELECT region, SUM(revenue) as revenue +FROM sales +GROUP BY region +``` + +## Common Patterns + +### useAnalyticsQuery (Custom UI Only) + +Use only when you need custom UI (cards/KPIs/conditional rendering): + +```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, null, 2)}
; +} +``` + +**Limitations:** +- No `enabled` option. Use conditional rendering to mount/unmount. +- No `refetch()`. Change `parameters` (memoized) or re-mount. + +### Avoid Double-Fetching + +```tsx +// ❌ Wrong: fetches the same query twice +const { data } = useAnalyticsQuery("spend_data", params); +return ; + +// ✅ Correct: let the chart fetch +return ; +``` + +## LLM Checklist + +Before finalizing code: + +- [ ] `package.json` has `"type": "module"` +- [ ] `dev` script uses `NODE_ENV=development tsx watch server/index.ts` +- [ ] `await createApp({ plugins: [...] })` is used +- [ ] Charts use props NOT children +- [ ] Query parameters are memoized with `useMemo` +- [ ] Loading/error/empty states are explicit diff --git a/experimental/aitools/lib/skills/apps/appkit/appkit-backend.md b/experimental/aitools/lib/skills/apps/appkit/appkit-backend.md new file mode 100644 index 0000000000..0a7b8278ec --- /dev/null +++ b/experimental/aitools/lib/skills/apps/appkit/appkit-backend.md @@ -0,0 +1,213 @@ +# AppKit Backend (@databricks/appkit) + +## 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 + +```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({})], +}); +``` + +### SQL Queries + +Put `.sql` files in `config/queries/`. Query key is filename without `.sql`: + +```sql +-- config/queries/spend_summary.sql +-- @param startDate DATE +-- @param endDate DATE +-- @param limit NUMERIC +SELECT * +FROM usage +WHERE usage_date BETWEEN :startDate AND :endDate +LIMIT :limit +``` + +**Supported `-- @param` types:** STRING, NUMERIC, BOOLEAN, DATE, TIMESTAMP, BINARY + +**Server-injected params:** +- `:workspaceId` is injected automatically and must NOT be annotated + +### HTTP Endpoints + +Mounted under `/api/analytics`: + +- `POST /api/analytics/query/:query_key` +- `POST /api/analytics/users/me/query/:query_key` +- `GET /api/analytics/arrow-result/:jobId` + +**Formats:** +- `format: "JSON"` (default) returns JSON rows +- `format: "ARROW"` returns Arrow statement_id over SSE + +## Execution Context + +### `asUser(req)` for User-Scoped Operations + +```ts +// Execute as the user (uses their Databricks permissions) +router.post("/users/me/data", async (req, res) => { + 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 + +- `getExecutionContext()`: Returns current context (user or service) +- `getCurrentUserId()`: Returns user ID in user context +- `getWorkspaceClient()`: Returns appropriate WorkspaceClient +- `getWarehouseId()`: `Promise` +- `getWorkspaceId()`: `Promise` +- `isInUserContext()`: Returns `true` if in user context + +## Custom Plugins + +```ts +import { Plugin, toPlugin } from "@databricks/appkit"; +import type express from "express"; + +class MyPlugin extends Plugin { + name = "my-plugin"; + envVars = []; + + 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 Cache + +```ts +await createApp({ + plugins: [server(), analytics({})], + cache: { + enabled: true, + ttl: 3600, // seconds + strictPersistence: false, + }, +}); +``` + +### Plugin-Level Cache + +```ts +// inside a Plugin subclass: +const value = await this.cache.getOrExecute( + ["my-plugin", "data", userId], + async () => expensiveWork(), + userKey, + { ttl: 300 }, +); +``` + +## Environment Variables + +### Required for Databricks Apps + +| Variable | Description | +|----------|-------------| +| `DATABRICKS_HOST` | Workspace URL | +| `DATABRICKS_APP_PORT` | Port to bind (default: 8000) | +| `DATABRICKS_APP_NAME` | App name in Databricks | + +### Required for SQL Queries + +| Variable | How to Set | +|----------|------------| +| `DATABRICKS_WAREHOUSE_ID` | In `app.yaml`: `valueFrom: sql-warehouse` | + +### Local Development Auth + +**Option 1: Databricks CLI Auth (recommended)** + +```bash +databricks auth login --host [host] --profile [profile-name] +DATABRICKS_CONFIG_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** + +```bash +# .env (add to .gitignore!) +DATABRICKS_HOST=https://xxx.cloud.databricks.com +DATABRICKS_TOKEN=dapi... +DATABRICKS_WAREHOUSE_ID=abc123... +``` diff --git a/experimental/aitools/lib/skills/apps/appkit/appkit-frontend.md b/experimental/aitools/lib/skills/apps/appkit/appkit-frontend.md new file mode 100644 index 0000000000..04e7bfb6df --- /dev/null +++ b/experimental/aitools/lib/skills/apps/appkit/appkit-frontend.md @@ -0,0 +1,229 @@ +# AppKit Frontend (@databricks/appkit-ui) + +## Imports + +```tsx +// React-facing APIs +import { useAnalyticsQuery, Card, Skeleton, BarChart } from "@databricks/appkit-ui/react"; + +// Non-React utilities (sql markers, arrow, SSE) +import { sql } from "@databricks/appkit-ui/js"; +``` + +## Charts + +All charts support: +- **Query mode**: `queryKey` + `parameters` +- **Data mode**: `data` (inline JSON, no server) + +**Available charts:** `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `DonutChart`, `HeatmapChart`, `ScatterChart`, `RadarChart` + +### Chart Props Reference + +```tsx + + + +``` + +### CRITICAL: Charts Do NOT Accept Children + +```tsx +// ❌ WRONG - AppKit charts are NOT Recharts wrappers +import { BarChart } from "@databricks/appkit-ui/react"; +import { Bar, XAxis, YAxis } from "recharts"; + + + // ❌ TypeScript error + // ❌ Not supported + + +// ✅ CORRECT - use props instead + +``` + +## SQL Helpers (`sql.*`) + +Use for typed parameters (returns `{ __sql_type, value }`): + +```ts +sql.string(value) // STRING +sql.number(value) // NUMERIC +sql.boolean(value) // BOOLEAN +sql.date(value) // DATE ("YYYY-MM-DD" or Date) +sql.timestamp(value) // TIMESTAMP +sql.binary(value) // STRING (hex) - use UNHEX(:param) in SQL +``` + +## useAnalyticsQuery + +```tsx +const { data, loading, error } = useAnalyticsQuery( + queryName: string, + params: Record +); +``` + +**Options:** +- `format?: "JSON" | "ARROW"` (default `"JSON"`) +- `autoStart?: boolean` (default `true`) + +**Limitations:** +- No `enabled` option - use conditional rendering +- No `refetch()` - change parameters or re-mount + +## useChartData + +```tsx +const { data, loading, error } = useChartData({ + queryKey: "my_query", + parameters: {}, + format: "auto", // "json" | "arrow" | "auto" + transformer: (data) => transformedData, +}); +``` + +## DataTable + +```tsx +import { DataTable } from "@databricks/appkit-ui/react"; + + +``` + +## connectSSE (Custom Streaming) + +```tsx +import { connectSSE } from "@databricks/appkit-ui/js"; + +connectSSE({ + url: endpoint, + payload: { key: "value" }, // POST if provided + onMessage: async ({ data }) => { /* handle */ }, + onError: (error) => { /* handle */ }, + signal: controller.signal, + maxRetries: 3, + retryDelay: 2000, + timeout: 300000, +}); +``` + +## ArrowClient (Advanced) + +```tsx +import { ArrowClient } from "@databricks/appkit-ui/js"; + +const table = await ArrowClient.processArrowBuffer(buffer); +const columns = ArrowClient.extractArrowColumns(table); +const { xData, yDataMap } = ArrowClient.extractChartData(table, "date", ["value"]); +``` + +## UI Components (Primitives) + +Import from `@databricks/appkit-ui/react`: + +**Layout:** `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` + +**Forms:** `Button`, `Input`, `Textarea`, `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `Checkbox`, `RadioGroup`, `Switch`, `Label` + +**Feedback:** `Skeleton`, `Spinner`, `Progress`, `Alert`, `Badge`, `Empty` + +**Overlays:** `Dialog`, `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogFooter`, `Popover`, `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`, `Sheet` + +**Navigation:** `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `Accordion`, `Breadcrumb`, `NavigationMenu` + +**Data Display:** `Table`, `DataTable`, `ScrollArea`, `Separator` + +### Radix Constraint + +`SelectItem` cannot have `value=""`. Use a sentinel value like `"all"`: + +```tsx +All items // ✅ +All items // ❌ Error +``` + +### TooltipProvider Requirement + +Wrap root with `TooltipProvider` if using tooltips: + +```tsx +import { TooltipProvider } from "@databricks/appkit-ui/react"; + +function App() { + return ( + + {/* Your app */} + + ); +} +``` + +## Stylesheet + +```css +/* main.css */ +@import "@databricks/appkit-ui/styles.css"; +``` + +### Theme Customization + +Override CSS variables for both light and dark modes: + +```css +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + /* ... see full list in llms.txt */ +} + +@media (prefers-color-scheme: dark) { + :root { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + /* ... */ + } +} +``` + +## SQL Result Types + +Databricks SQL JSON results may return numeric fields as strings (especially `DECIMAL`). Convert explicitly: + +```ts +const value = Number(row.amount); +``` + +For large datasets, prefer `format: "ARROW"` for better numeric fidelity. diff --git a/experimental/aitools/lib/skills/apps/appkit/appkit-scaffolding.md b/experimental/aitools/lib/skills/apps/appkit/appkit-scaffolding.md new file mode 100644 index 0000000000..dabf46315d --- /dev/null +++ b/experimental/aitools/lib/skills/apps/appkit/appkit-scaffolding.md @@ -0,0 +1,208 @@ +# AppKit Project Scaffolding + +## Canonical Project Layout + +``` +my-app/ +├── server/ +│ ├── index.ts # backend entry point +│ └── .env # local dev env vars (gitignore) +├── client/ +│ ├── index.html +│ ├── vite.config.ts +│ └── src/ +│ ├── main.tsx +│ └── App.tsx +├── config/ +│ └── queries/ +│ └── my_query.sql +├── app.yaml +├── package.json +└── tsconfig.json +``` + +## 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/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, analytics } from "@databricks/appkit"; + +await createApp({ + plugins: [server(), analytics({})], +}); +``` + +## app.yaml (Databricks Apps Config) + +```yaml +command: + - node + - build/index.mjs +env: + - name: DATABRICKS_WAREHOUSE_ID + valueFrom: sql-warehouse +``` + +## Type Generation + +### Vite Plugin + +```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"], + }), + ], +}); +``` + +### CLI + +```bash +npx appkit-generate-types [rootDir] [outFile] [warehouseId] +npx appkit-generate-types . client/src/appKitTypes.d.ts +npx appkit-generate-types --no-cache # Force regeneration +``` + +## Running the App + +```bash +# Install dependencies +npm install + +# Development (starts backend + Vite dev server) +npm run dev + +# Production build +npm run build +npm start +``` + +## Adding to Existing React/Vite App + +1. Install dependencies: +```bash +npm install @databricks/appkit @databricks/appkit-ui react react-dom +npm install -D tsx tsdown vite @vitejs/plugin-react typescript +``` + +2. Move Vite app to `client/` folder: +``` +client/index.html +client/vite.config.ts +client/src/ +``` + +3. Create `server/index.ts`: +```ts +import { createApp, server, analytics } from "@databricks/appkit"; + +await createApp({ + plugins: [server(), analytics({})], +}); +``` + +4. Update `package.json` scripts as shown above diff --git a/experimental/aitools/templates/appkit/template/{{.project_name}}/CLAUDE.md b/experimental/aitools/templates/appkit/template/{{.project_name}}/CLAUDE.md index 2ca2b04721..f25543e36f 100644 --- a/experimental/aitools/templates/appkit/template/{{.project_name}}/CLAUDE.md +++ b/experimental/aitools/templates/appkit/template/{{.project_name}}/CLAUDE.md @@ -34,7 +34,15 @@ export const querySchemas = { }; ``` -**Step 3: Add visualization to your app** +**Step 3: Run typegen (REQUIRED after any schema change)** + +```bash +npm run typegen +``` + +This regenerates `client/src/appKitTypes.d.ts` with your new query types. **Without this step, TypeScript will not recognize your query keys and builds will fail.** + +**Step 4: Add visualization to your app** ```typescript // client/src/App.tsx @@ -45,9 +53,9 @@ import { BarChart } from '@databricks/appkit-ui/react'; **That's it!** The component handles data fetching, loading states, and rendering automatically. -**To refresh TypeScript types after adding queries:** -- Run `npm run typegen` OR run `npm run dev` - both auto-generate type definitions in `client/src/appKitTypes.d.ts` -- DO NOT manually edit `appKitTypes.d.ts` +**⚠️ CRITICAL: Always run `npm run typegen` after modifying `config/queries/schema.ts`** +- DO NOT manually edit `client/src/appKitTypes.d.ts` - it is auto-generated +- If you see errors like `'"my_query"' is not assignable to parameter of type`, run `npm run typegen` ## Installation diff --git a/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/appkit-sdk.md b/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/appkit-sdk.md index 5ab00768e1..4675db6b2b 100644 --- a/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/appkit-sdk.md +++ b/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/appkit-sdk.md @@ -45,12 +45,11 @@ Use cases: - Data that needs transformation before display ```typescript -import { useAnalyticsQuery, Skeleton } from '@databricks/app-kit-ui/react'; - -interface QueryResult { column_name: string; value: number; } +import { useAnalyticsQuery, Skeleton } from '@databricks/appkit-ui/react'; +import { sql } from '@databricks/appkit-ui/js'; function CustomDisplay() { - const { data, loading, error } = useAnalyticsQuery('query_name', { + const { data, loading, error } = useAnalyticsQuery('query_name', { start_date: sql.date(Date.now()), category: sql.string("tools") }); diff --git a/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/frontend.md b/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/frontend.md index a270b46b9e..bdb3498c1d 100644 --- a/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/frontend.md +++ b/experimental/aitools/templates/appkit/template/{{.project_name}}/docs/frontend.md @@ -35,23 +35,23 @@ function MyDashboard() { Components automatically fetch data, show loading states, display errors, and render with sensible defaults. -**Custom Visualization (Recharts):** +**⚠️ CRITICAL: AppKit charts do NOT support children** ```typescript +// ❌ WRONG - AppKit charts don't accept Recharts children import { BarChart } from '@databricks/appkit-ui/react'; -import { Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'; - - - - - - - - - +import { Bar, XAxis, YAxis } from 'recharts'; + + // TypeScript error! + // TypeScript error! + +// ✅ CORRECT - Use simple self-closing syntax + ``` +AppKit charts auto-configure axes, tooltips, and styling. Do NOT import Recharts components. + Databricks brand colors: `['#40d1f5', '#4462c9', '#EB1600', '#0B2026', '#4A4A4A', '#353a4a']` **❌ Don't double-fetch:**