diff --git a/.github/workflows/release-lakebase.yml b/.github/workflows/release-lakebase.yml new file mode 100644 index 00000000..352bf990 --- /dev/null +++ b/.github/workflows/release-lakebase.yml @@ -0,0 +1,69 @@ +name: Release @databricks/lakebase + +on: + push: + branches: + - main + paths: + - 'packages/lakebase/**' + workflow_dispatch: + inputs: + dry-run: + description: "Dry run (no actual release)" + required: false + type: boolean + default: false + +jobs: + release: + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + + environment: release + + env: + DRY_RUN: ${{ inputs.dry-run == true }} + + permissions: + contents: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + + - name: Update npm + run: npm install -g npm@latest + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Release + working-directory: packages/lakebase + run: | + if [ "$DRY_RUN" == "true" ]; then + pnpm release:dry + else + pnpm release:ci + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ff58ac8..86547abd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,9 @@ jobs: environment: release + outputs: + version: ${{ steps.version.outputs.version }} + permissions: contents: write id-token: write @@ -61,6 +64,18 @@ jobs: echo "dry_run=${{ inputs.dry-run }}" >> $GITHUB_OUTPUT fi + - name: Determine version + id: version + if: steps.mode.outputs.dry_run != 'true' + run: | + VERSION=$(pnpm exec release-it --release-version --ci 2>/dev/null) || true + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Next version: $VERSION" + else + echo "No releasable version detected" + fi + - name: Release run: | if [ "${{ steps.mode.outputs.dry_run }}" == "true" ]; then @@ -70,3 +85,43 @@ jobs: fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + sync-template: + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + + needs: release + # in case a dry run is performed, the version is not set so we need to check for it. + if: needs.release.outputs.version != '' + + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Sync template and push tag + run: pnpm exec tsx tools/publish-template-tag.ts ${{ needs.release.outputs.version }} diff --git a/.release-it.json b/.release-it.json index 980cc513..b3a56377 100644 --- a/.release-it.json +++ b/.release-it.json @@ -3,15 +3,19 @@ "git": { "commitMessage": "chore: release v${version} [skip ci]", "tagName": "v${version}", + "tagMatch": "v*", "tagAnnotation": "Release v${version}", "requireBranch": "main", "requireCleanWorkingDir": true, + "requireCommits": true, + "requireCommitsFail": false, + "commitsPath": "packages/appkit packages/appkit-ui packages/shared", "push": true, "pushArgs": ["--follow-tags"] }, "github": { "release": true, - "releaseName": "v${version}", + "releaseName": "AppKit v${version}", "autoGenerate": false, "draft": false, "preRelease": false, @@ -35,7 +39,13 @@ "commitsSort": ["type", "subject"] }, "infile": "CHANGELOG.md", - "header": "# Changelog\n\nAll notable changes to this project will be documented in this file." + "header": "# Changelog\n\nAll notable changes to this project will be documented in this file.", + "gitRawCommitsOpts": { + "path": ["packages/appkit", "packages/appkit-ui", "packages/shared"] + }, + "commitsOpts": { + "path": ["packages/appkit", "packages/appkit-ui", "packages/shared"] + } } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index f8531183..b6871d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. +## [0.7.4](https://github.com/databricks/appkit/compare/v0.7.3...v0.7.4) (2026-02-18) + +* typegen command ([#119](https://github.com/databricks/appkit/issues/119)) ([8c3735c](https://github.com/databricks/appkit/commit/8c3735c6b27c5e5cbacb81363ccaf4f6acbfd185)) + +## [0.7.3](https://github.com/databricks/appkit/compare/v0.7.2...v0.7.3) (2026-02-18) + +* release `@databricks/lakebase` correctly, fix `appkit` dependency ([#111](https://github.com/databricks/appkit/issues/111)) ([5b6856a](https://github.com/databricks/appkit/commit/5b6856a6b42680a671cfc3e99eab3bef72803fd1)) + +## [0.7.2](https://github.com/databricks/appkit/compare/v0.7.1...v0.7.2) (2026-02-18) + +* template sync ([#109](https://github.com/databricks/appkit/issues/109)) ([f250016](https://github.com/databricks/appkit/commit/f250016b28e24e3ce56d09a0a3d95088a689a943)) + +## [0.7.1](https://github.com/databricks/appkit/compare/v0.7.0...v0.7.1) (2026-02-18) + +* sync template versions on release ([#105](https://github.com/databricks/appkit/issues/105)) ([4cbe826](https://github.com/databricks/appkit/commit/4cbe8266e80e4b4cfe4b4e0594c2633dcba7123a)) + +## [0.7.0](https://github.com/databricks/appkit/compare/v0.6.0...v0.7.0) (2026-02-17) + +* introduce Lakebase Autoscaling driver ([#98](https://github.com/databricks/appkit/issues/98)) ([27b1848](https://github.com/databricks/appkit/commit/27b184886b2ab15c73f3d46f5ff9e9c6d8806c71)) + ## [0.6.0](https://github.com/databricks/appkit/compare/v0.5.4...v0.6.0) (2026-02-16) ### appkit diff --git a/CLAUDE.md b/CLAUDE.md index 1f380700..9a17f870 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -233,6 +233,39 @@ The AnalyticsPlugin provides SQL query execution: - Built-in caching with configurable TTL - Databricks SQL Warehouse connector for execution +### Lakebase Autoscaling Connector + +**Location:** `packages/appkit/src/connectors/lakebase/` + +AppKit provides `createLakebasePool()` - a factory function that returns a standard `pg.Pool` configured with automatic OAuth token refresh for Databricks Lakebase (OLTP) databases. + +**Key Features:** +- Returns standard `pg.Pool` (compatible with all ORMs) +- Automatic OAuth token refresh (1-hour tokens, 2-minute buffer) +- Token caching to minimize API calls +- Battle-tested pattern (same as AWS RDS IAM authentication) + +**Quick Example:** +```typescript +import { createLakebasePool } from '@databricks/appkit'; + +// Reads from PGHOST, PGDATABASE, LAKEBASE_ENDPOINT env vars +const pool = createLakebasePool(); + +// Standard pg.Pool API +const result = await pool.query('SELECT * FROM users'); +``` + +**ORM Integration:** +Works with Drizzle, Prisma, TypeORM - see [Lakebase Integration Docs](docs/docs/integrations/lakebase.md) for examples. + +**Architecture:** +- Connector files: `packages/appkit/src/connectors/lakebase/` + - `pool.ts` - Pool factory with OAuth token refresh + - `types.ts` - TypeScript interfaces (`LakebasePoolConfig`) + - `utils.ts` - Helper functions (`generateDatabaseCredential`) + - `auth-types.ts` - Lakebase v2 API types + ### Frontend-Backend Interaction ``` diff --git a/NOTICE.md b/NOTICE.md index f1c83b4c..55513f1d 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -67,7 +67,7 @@ This Software contains code from the following open source projects: | [lucide-react](https://www.npmjs.com/package/lucide-react) | 0.554.0 | ISC | https://lucide.dev | | [next-themes](https://www.npmjs.com/package/next-themes) | 0.4.6 | MIT | https://github.com/pacocoursey/next-themes#readme | | [obug](https://www.npmjs.com/package/obug) | 2.1.1 | MIT | https://github.com/sxzz/obug#readme | -| [pg](https://www.npmjs.com/package/pg) | 8.16.3 | MIT | https://github.com/brianc/node-postgres | +| [pg](https://www.npmjs.com/package/pg) | 8.18.0 | MIT | https://github.com/brianc/node-postgres | | [react-day-picker](https://www.npmjs.com/package/react-day-picker) | 9.12.0 | MIT | https://daypicker.dev | | [react-hook-form](https://www.npmjs.com/package/react-hook-form) | 7.68.0 | MIT | https://react-hook-form.com | | [react-resizable-panels](https://www.npmjs.com/package/react-resizable-panels) | 3.0.6 | MIT | https://github.com/bvaughn/react-resizable-panels#readme | diff --git a/README.md b/README.md index aed20ac6..770e7eb9 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Build Databricks Apps faster with our brand-new Node.js + React SDK. Built for humans and AI. -> [!WARNING] PREVIEW - NOT FOR PRODUCTION USE -> +> [!WARNING] +> PREVIEW - NOT FOR PRODUCTION USE + > **This SDK is in preview and is subject to change without notice.** > > - ❌ **Do NOT use in production environments** @@ -24,6 +25,23 @@ AppKit simplifies building data applications on Databricks by providing: - **Developer experience**: Remote hot reload, file-based queries, optimized for AI-assisted development - **Databricks native**: Seamless integration with SQL Warehouses, Unity Catalog, and other workspace resources +## Plugins + +AppKit's power comes from its plugin system. Each plugin adds a focused capability to your app with minimal configuration. + +### Available now + +- **Analytics Plugin** — Query your Lakehouse data directly from your app. Define SQL queries as files, execute them against Databricks SQL Warehouses, and get automatic caching, parameterization, and on-behalf-of user execution out of the box. Perfect for building apps that surface insights from your Lakehouse. + +### Coming soon + +- **Genie Plugin** — Conversational AI interface powered by Databricks Genie +- **Files Plugin** — Browse, upload, and manage files in Unity Catalog Volumes +- **Lakebase Plugin** — OLTP database operations with automatic OAuth token management +- ...and this is just the beginning. + +> Missing a plugin? [Open an issue](https://github.com/databricks/appkit/issues/new) and tell us what you need — community input directly shapes the roadmap. + ## Getting started Follow the [Getting Started](https://databricks.github.io/appkit/docs/) guide to get started with AppKit. diff --git a/apps/dev-playground/.env.dist b/apps/dev-playground/.env.dist index 7a7076ff..135da823 100644 --- a/apps/dev-playground/.env.dist +++ b/apps/dev-playground/.env.dist @@ -6,3 +6,8 @@ NODE_ENV='development' OTEL_EXPORTER_OTLP_ENDPOINT='http://localhost:4318' OTEL_RESOURCE_ATTRIBUTES='service.sample_attribute=dev' OTEL_SERVICE_NAME='dev-playground' +LAKEBASE_ENDPOINT='' # Run: databricks postgres list-endpoints projects/{project-id}/branches/{branch-id} — use the `name` field from the output +PGHOST= +PGUSER= +PGDATABASE=databricks_postgres +PGSSLMODE=require diff --git a/apps/dev-playground/client/package-lock.json b/apps/dev-playground/client/package-lock.json index a0cd6cd5..22b20739 100644 --- a/apps/dev-playground/client/package-lock.json +++ b/apps/dev-playground/client/package-lock.json @@ -113,6 +113,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2615,6 +2616,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.134.9.tgz", "integrity": "sha512-JIxFamShs3gRIkOxpgz/3bglbSKZHMrzKASwNFg+sQPVXVPOLtN35D5PuEDAFTPPht9Wv48WWUNYE03ZytnNug==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.133.28", "@tanstack/react-store": "^0.8.0", @@ -2722,6 +2724,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.134.9.tgz", "integrity": "sha512-9Vr8tYC59I70DYGVRknRf4vjQMjSfHvmc+iTM8vcpwERBh3Vgkv90f8ol85KHKqjorSsCqMeYFhFt8AM4A4CSw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/history": "1.133.28", "@tanstack/store": "^0.8.0", @@ -3061,6 +3064,7 @@ "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -3071,6 +3075,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3081,6 +3086,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3144,6 +3150,7 @@ "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", @@ -3402,6 +3409,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3625,6 +3633,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3923,7 +3932,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -4223,6 +4233,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5590,6 +5601,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5678,6 +5690,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5687,6 +5700,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5706,6 +5720,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -5874,7 +5889,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -6041,6 +6057,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -6193,7 +6210,8 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -6222,7 +6240,8 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tiny-warning": { "version": "1.0.3", @@ -6268,6 +6287,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6364,6 +6384,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6653,6 +6674,7 @@ "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz", "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.92.0", "fdir": "^6.5.0", @@ -6745,6 +6767,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/apps/dev-playground/client/src/appKitTypes.d.ts b/apps/dev-playground/client/src/appKitTypes.d.ts index 0e0ae0b0..049fae9f 100644 --- a/apps/dev-playground/client/src/appKitTypes.d.ts +++ b/apps/dev-playground/client/src/appKitTypes.d.ts @@ -14,46 +14,28 @@ declare module "@databricks/appkit-ui/react" { endDate: SQLDateMarker; }; result: Array<{ - /** @sqlType STRING */ - app_name: string; - /** @sqlType STRING */ - day_of_week: string; - /** @sqlType DECIMAL(35,2) */ - spend: number; + ; }>; }; apps_list: { name: "apps_list"; parameters: Record; result: Array<{ - /** @sqlType STRING */ - id: string; - /** @sqlType STRING */ - name: string; - /** @sqlType STRING */ - creator: string; - /** @sqlType STRING */ - tags: string; - /** @sqlType DECIMAL(38,6) */ - totalSpend: number; - /** @sqlType DATE */ - createdAt: string; + ; }>; }; cost_recommendations: { name: "cost_recommendations"; parameters: Record; result: Array<{ - /** @sqlType INT */ - dummy: number; + ; }>; }; example: { name: "example"; parameters: Record; result: Array<{ - /** @sqlType BOOLEAN */ - "(1 = 1)": boolean; + ; }>; }; spend_data: { @@ -73,12 +55,7 @@ declare module "@databricks/appkit-ui/react" { creator: SQLStringMarker; }; result: Array<{ - /** @sqlType STRING */ - group_key: string; - /** @sqlType TIMESTAMP */ - aggregation_period: string; - /** @sqlType DECIMAL(38,6) */ - cost_usd: number; + ; }>; }; spend_summary: { @@ -92,12 +69,7 @@ declare module "@databricks/appkit-ui/react" { startDate: SQLDateMarker; }; result: Array<{ - /** @sqlType DECIMAL(33,0) */ - total: number; - /** @sqlType DECIMAL(33,0) */ - average: number; - /** @sqlType DECIMAL(33,0) */ - forecasted: number; + ; }>; }; sql_helpers_test: { @@ -117,22 +89,7 @@ declare module "@databricks/appkit-ui/react" { binaryParam: SQLStringMarker; }; result: Array<{ - /** @sqlType STRING */ - string_value: string; - /** @sqlType STRING */ - number_value: string; - /** @sqlType STRING */ - boolean_value: string; - /** @sqlType STRING */ - date_value: string; - /** @sqlType STRING */ - timestamp_value: string; - /** @sqlType BINARY */ - binary_value: string; - /** @sqlType STRING */ - binary_hex: string; - /** @sqlType INT */ - binary_length: number; + ; }>; }; top_contributors: { @@ -146,10 +103,7 @@ declare module "@databricks/appkit-ui/react" { endDate: SQLDateMarker; }; result: Array<{ - /** @sqlType STRING */ - app_name: string; - /** @sqlType DECIMAL(38,6) */ - total_cost_usd: number; + ; }>; }; untagged_apps: { @@ -163,14 +117,7 @@ declare module "@databricks/appkit-ui/react" { endDate: SQLDateMarker; }; result: Array<{ - /** @sqlType STRING */ - app_name: string; - /** @sqlType STRING */ - creator: string; - /** @sqlType DECIMAL(38,6) */ - total_cost_usd: number; - /** @sqlType DECIMAL(38,10) */ - avg_period_cost_usd: number; + ; }>; }; } diff --git a/apps/dev-playground/client/src/components/lakebase/ActivityLogsPanel.tsx b/apps/dev-playground/client/src/components/lakebase/ActivityLogsPanel.tsx new file mode 100644 index 00000000..10bb6406 --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/ActivityLogsPanel.tsx @@ -0,0 +1,259 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, +} from "@databricks/appkit-ui/react"; +import { Activity, Loader2 } from "lucide-react"; +import { useId, useState } from "react"; +import { useLakebaseData, useLakebasePost } from "@/hooks/use-lakebase-data"; + +interface ActivityLog { + id: number; + userId: string; + action: string; + metadata: Record | null; + timestamp: string; +} + +interface Stats { + totalLogs: number; + uniqueUsers: number; + recentActivity: number; +} + +export function ActivityLogsPanel() { + const userIdFieldId = useId(); + const actionFieldId = useId(); + + const { + data: logs, + loading: logsLoading, + error: logsError, + refetch, + } = useLakebaseData("/api/lakebase-examples/drizzle/activity"); + + const { data: stats } = useLakebaseData( + "/api/lakebase-examples/drizzle/stats", + ); + + const { post, loading: creating } = useLakebasePost< + Partial, + ActivityLog + >("/api/lakebase-examples/drizzle/activity"); + + const generateRandomActivity = () => { + const users = ["alice", "bob", "charlie", "diana", "eve"]; + const actions = [ + "login", + "logout", + "view_dashboard", + "create_report", + "export_data", + "update_settings", + "share_document", + "delete_item", + ]; + + return { + userId: users[Math.floor(Math.random() * users.length)], + action: actions[Math.floor(Math.random() * actions.length)], + }; + }; + + const [formData, setFormData] = useState(generateRandomActivity()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + userId: formData.userId, + action: formData.action, + metadata: { + source: "web", + timestamp: new Date().toISOString(), + }, + }); + + if (result) { + setFormData(generateRandomActivity()); + refetch(); + } + }; + + return ( +
+ {/* Header */} + + +
+
+ +
+
+ Drizzle ORM Example + + Type-safe queries with schema definitions and automatic type + inference + +
+
+
+
+ + {/* Statistics */} + {stats && ( +
+ + + Total Logs + {stats.totalLogs} + + + + + + Unique Users + + {stats.uniqueUsers} + + + + + + Last 24 Hours + + {stats.recentActivity} + + +
+ )} + + {/* Create log form */} + + + Log Activity + + +
+
+
+ + + setFormData({ ...formData, userId: e.target.value }) + } + placeholder="alice" + required + /> +
+
+ + + setFormData({ ...formData, action: e.target.value }) + } + placeholder="view_dashboard" + required + /> +
+
+ +
+
+
+ + {/* Activity logs */} + + +
+ Activity Logs + +
+
+ + {logsLoading && ( +
+
+ Loading activity logs... +
+ )} + + {logsError && ( +
+ Error: {logsError.message} +
+ )} + + {logs && logs.length === 0 && ( +
+ +

No activity logs yet. Log your first activity above.

+
+ )} + + {logs && logs.length > 0 && ( +
+ {logs.map((log) => ( +
+
+
+ + {log.userId} + + {log.action} +
+ + {new Date(log.timestamp).toLocaleString()} + +
+ {log.metadata && ( +
+ + View metadata + +
+                        {JSON.stringify(log.metadata, null, 2)}
+                      
+
+ )} +
+ ))} +
+ )} + + +
+ ); +} diff --git a/apps/dev-playground/client/src/components/lakebase/OrdersPanel.tsx b/apps/dev-playground/client/src/components/lakebase/OrdersPanel.tsx new file mode 100644 index 00000000..8730bb04 --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/OrdersPanel.tsx @@ -0,0 +1,524 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, +} from "@databricks/appkit-ui/react"; +import { + CheckCircle, + Circle, + Loader2, + Package, + ShoppingCart, + Truck, +} from "lucide-react"; +import { useId, useState } from "react"; +import { + useLakebaseData, + useLakebasePatch, + useLakebasePost, +} from "@/hooks/use-lakebase-data"; + +interface Order { + id: number; + orderNumber: string; + customerName: string; + productName: string; + amount: number; + status: "pending" | "processing" | "shipped" | "delivered"; + createdAt: string; + updatedAt: string; +} + +interface OrderStats { + total: number; + pending: number; + processing: number; + shipped: number; + delivered: number; +} + +export function OrdersPanel() { + const orderNumberId = useId(); + const customerNameId = useId(); + const productNameId = useId(); + const amountId = useId(); + + const { + data: orders, + loading: ordersLoading, + error: ordersError, + refetch, + } = useLakebaseData("/api/lakebase-examples/sequelize/orders"); + + const { data: stats } = useLakebaseData( + "/api/lakebase-examples/sequelize/stats", + ); + + const { post, loading: creating } = useLakebasePost, Order>( + "/api/lakebase-examples/sequelize/orders", + ); + + const { patch, loading: updating } = useLakebasePatch< + { status: string }, + Order + >("/api/lakebase-examples/sequelize/orders"); + + const generateRandomOrder = () => { + const customers = [ + "Alice Johnson", + "Bob Smith", + "Carol Williams", + "David Brown", + "Emma Davis", + "Frank Miller", + "Grace Wilson", + "Henry Moore", + ]; + const products = [ + "Wireless Bluetooth Headphones", + "USB-C Charging Cable", + "Laptop Stand - Ergonomic", + "Mechanical Keyboard - RGB", + "4K Webcam with Microphone", + "Portable SSD 1TB", + "Wireless Mouse - Ergonomic", + "Monitor Arm Mount", + "Noise Cancelling Earbuds", + "Desk Lamp - LED", + ]; + + const orderNum = `ORD-${new Date().getFullYear()}-${String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0")}`; + const customer = customers[Math.floor(Math.random() * customers.length)]; + const product = products[Math.floor(Math.random() * products.length)]; + const amount = (Math.random() * 200 + 10).toFixed(2); + + return { + orderNumber: orderNum, + customerName: customer, + productName: product, + amount, + }; + }; + + const [formData, setFormData] = useState(generateRandomOrder()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + orderNumber: formData.orderNumber, + customerName: formData.customerName, + productName: formData.productName, + amount: Number.parseFloat(formData.amount), + status: "pending", + }); + + if (result) { + setFormData(generateRandomOrder()); + refetch(); + } + }; + + const handleStatusUpdate = async (id: number, status: Order["status"]) => { + const result = await patch(id, { status }); + if (result) { + refetch(); + } + }; + + const getStatusBadge = (status: Order["status"]) => { + switch (status) { + case "pending": + return ( + + + Pending + + ); + case "processing": + return ( + + + Processing + + ); + case "shipped": + return ( + + + Shipped + + ); + case "delivered": + return ( + + + Delivered + + ); + } + }; + + const ordersByStatus = orders + ? { + pending: orders.filter((o) => o.status === "pending"), + processing: orders.filter((o) => o.status === "processing"), + shipped: orders.filter((o) => o.status === "shipped"), + delivered: orders.filter((o) => o.status === "delivered"), + } + : { pending: [], processing: [], shipped: [], delivered: [] }; + + return ( +
+ {/* Header */} + + +
+
+ +
+
+ Sequelize Example + + Model-based ORM with intuitive API and automatic timestamps + +
+
+
+
+ + {/* Statistics */} + {stats && ( +
+ + + + Total Orders + + {stats.total} + + + + + Pending + {stats.pending} + + + + + Processing + {stats.processing} + + + + + Shipped + {stats.shipped} + + + + + Delivered + {stats.delivered} + + +
+ )} + + {/* Create order form */} + + + Create Order + + +
+
+
+ + + setFormData({ ...formData, orderNumber: e.target.value }) + } + placeholder="ORD-2024-0001" + required + /> +
+
+ + + setFormData({ ...formData, customerName: e.target.value }) + } + placeholder="John Doe" + required + /> +
+
+
+
+ + + setFormData({ ...formData, productName: e.target.value }) + } + placeholder="Wireless Headphones" + required + /> +
+
+ + + setFormData({ ...formData, amount: e.target.value }) + } + placeholder="99.99" + required + /> +
+
+ +
+
+
+ + {/* Order board */} + + +
+ Order Board + +
+
+ + {ordersLoading && ( +
+
+ Loading orders... +
+ )} + + {ordersError && ( +
+ Error:{" "} + {ordersError.message} +
+ )} + + {orders && orders.length === 0 && ( +
+ +

No orders yet. Add an order to get started.

+
+ )} + + {orders && orders.length > 0 && ( +
+ {/* Pending column */} +
+
+ Pending ({ordersByStatus.pending.length}) +
+
+ {ordersByStatus.pending.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+ +
+
+ ))} +
+
+ + {/* Processing column */} +
+
+ Processing ({ordersByStatus.processing.length}) +
+
+ {ordersByStatus.processing.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+
+ + +
+
+
+ ))} +
+
+ + {/* Shipped column */} +
+
+ Shipped ({ordersByStatus.shipped.length}) +
+
+ {ordersByStatus.shipped.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+ +
+
+ ))} +
+
+ + {/* Delivered column */} +
+
+ Delivered ({ordersByStatus.delivered.length}) +
+
+ {ordersByStatus.delivered.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+
+
+ ))} +
+
+
+ )} + + +
+ ); +} diff --git a/apps/dev-playground/client/src/components/lakebase/ProductsPanel.tsx b/apps/dev-playground/client/src/components/lakebase/ProductsPanel.tsx new file mode 100644 index 00000000..d1b6c690 --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/ProductsPanel.tsx @@ -0,0 +1,305 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, +} from "@databricks/appkit-ui/react"; +import { Database, Loader2, Package } from "lucide-react"; +import { useId, useState } from "react"; +import { useLakebaseData, useLakebasePost } from "@/hooks/use-lakebase-data"; + +interface Product { + id: number; + name: string; + category: string; + price: number | string; // PostgreSQL DECIMAL returns as string + stock: number; + created_by?: string; + created_at: string; +} + +interface CreateProductRequest { + name: string; + category: string; + price: number; + stock: number; +} + +interface HealthStatus { + status: string; + connected: boolean; + message: string; +} + +export function ProductsPanel() { + const nameId = useId(); + const categoryId = useId(); + const priceId = useId(); + const stockId = useId(); + + const { + data: products, + loading: productsLoading, + error: productsError, + refetch, + } = useLakebaseData("/api/lakebase-examples/raw/products"); + + const { data: health } = useLakebaseData( + "/api/lakebase-examples/raw/health", + ); + + const { post, loading: creating } = useLakebasePost< + CreateProductRequest, + Product + >("/api/lakebase-examples/raw/products"); + + const generateRandomProduct = () => { + const products = [ + "Ergonomic Keyboard", + "Wireless Mouse", + "USB-C Hub", + "Laptop Stand", + "Monitor Arm", + "Mechanical Keyboard", + "Gaming Headset", + "Webcam HD", + ]; + const categories = ["Electronics", "Accessories", "Peripherals", "Office"]; + const price = (Math.random() * (199.99 - 29.99) + 29.99).toFixed(2); + const stock = Math.floor(Math.random() * (500 - 50) + 50); + + return { + name: products[Math.floor(Math.random() * products.length)], + category: categories[Math.floor(Math.random() * categories.length)], + price, + stock: String(stock), + }; + }; + + const [formData, setFormData] = useState(generateRandomProduct()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + name: formData.name, + category: formData.category, + price: Number(formData.price), + stock: Number(formData.stock), + }); + + if (result) { + setFormData(generateRandomProduct()); + refetch(); + } + }; + + return ( +
+ {/* Header with connection status */} + + +
+
+
+ +
+
+ Raw Driver Example + + Direct PostgreSQL connection using pg.Pool with automatic + OAuth token refresh + +
+
+ {health && ( + + {health.connected ? "Connected" : "Disconnected"} + + )} +
+
+
+ + {/* Create product form */} + + + Create Product + + +
+
+
+ + + setFormData({ ...formData, name: e.target.value }) + } + placeholder="Wireless Mouse" + required + /> +
+
+ + + setFormData({ ...formData, category: e.target.value }) + } + placeholder="Electronics" + required + /> +
+
+ + + setFormData({ ...formData, price: e.target.value }) + } + placeholder="29.99" + required + /> +
+
+ + + setFormData({ ...formData, stock: e.target.value }) + } + placeholder="100" + required + /> +
+
+ +
+
+
+ + {/* Products list */} + + +
+ Products Catalog + +
+
+ + {productsLoading && ( +
+
+ Loading products... +
+ )} + + {productsError && ( +
+ Error:{" "} + {productsError.message} +
+ )} + + {products && products.length === 0 && ( +
+ +

No products available. Create your first product above.

+
+ )} + + {products && products.length > 0 && ( +
+ + + + + + + + + + + + {products.map((product) => ( + + + + + + + + ))} + +
+ ID + + Name + + Category + + Price + + Stock +
{product.id}{product.name} + {product.category} + + ${Number(product.price).toFixed(2)} + + {product.stock} +
+
+ )} + + +
+ ); +} diff --git a/apps/dev-playground/client/src/components/lakebase/TasksPanel.tsx b/apps/dev-playground/client/src/components/lakebase/TasksPanel.tsx new file mode 100644 index 00000000..fdc5c19a --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/TasksPanel.tsx @@ -0,0 +1,401 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, + Textarea, +} from "@databricks/appkit-ui/react"; +import { CheckCircle, Circle, ListTodo, Loader2 } from "lucide-react"; +import { useId, useState } from "react"; +import { + useLakebaseData, + useLakebasePatch, + useLakebasePost, +} from "@/hooks/use-lakebase-data"; + +interface Task { + id: number; + title: string; + status: "pending" | "in_progress" | "completed"; + description: string | null; + createdAt: string; +} + +interface TaskStats { + total: number; + pending: number; + inProgress: number; + completed: number; +} + +export function TasksPanel() { + const titleId = useId(); + const descriptionId = useId(); + + const { + data: tasks, + loading: tasksLoading, + error: tasksError, + refetch, + } = useLakebaseData("/api/lakebase-examples/typeorm/tasks"); + + const { data: stats } = useLakebaseData( + "/api/lakebase-examples/typeorm/stats", + ); + + const { post, loading: creating } = useLakebasePost, Task>( + "/api/lakebase-examples/typeorm/tasks", + ); + + const { patch, loading: updating } = useLakebasePatch< + { status: string }, + Task + >("/api/lakebase-examples/typeorm/tasks"); + + const generateRandomTask = () => { + const tasks = [ + { + title: "Implement user authentication", + description: "Add OAuth2 authentication flow with JWT tokens", + }, + { + title: "Write API documentation", + description: "Document all REST endpoints with examples", + }, + { + title: "Set up CI/CD pipeline", + description: "Configure GitHub Actions for automated testing", + }, + { + title: "Add error monitoring", + description: "Integrate error tracking and alerting system", + }, + { + title: "Optimize database queries", + description: "Add indexes and analyze slow queries", + }, + { + title: "Implement data validation", + description: "Add schema validation for all API requests", + }, + { + title: "Set up development environment", + description: "Configure local development tools and dependencies", + }, + { + title: "Design database schema", + description: "Create ERD and define table relationships", + }, + ]; + + const task = tasks[Math.floor(Math.random() * tasks.length)]; + return { + title: task.title, + description: task.description, + }; + }; + + const [formData, setFormData] = useState(generateRandomTask()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + title: formData.title, + description: formData.description || null, + status: "pending", + }); + + if (result) { + setFormData(generateRandomTask()); + refetch(); + } + }; + + const handleStatusUpdate = async (id: number, status: Task["status"]) => { + const result = await patch(id, { status }); + if (result) { + refetch(); + } + }; + + const getStatusBadge = (status: Task["status"]) => { + switch (status) { + case "pending": + return ( + + + Pending + + ); + case "in_progress": + return ( + + + In Progress + + ); + case "completed": + return ( + + + Completed + + ); + } + }; + + const tasksByStatus = tasks + ? { + pending: tasks.filter((t) => t.status === "pending"), + in_progress: tasks.filter((t) => t.status === "in_progress"), + completed: tasks.filter((t) => t.status === "completed"), + } + : { pending: [], in_progress: [], completed: [] }; + + return ( +
+ {/* Header */} + + +
+
+ +
+
+ TypeORM Example + + Entity-based data access with decorators and repository pattern + +
+
+
+
+ + {/* Statistics */} + {stats && ( +
+ + + Total Tasks + {stats.total} + + + + + Pending + {stats.pending} + + + + + In Progress + {stats.inProgress} + + + + + Completed + {stats.completed} + + +
+ )} + + {/* Create task form */} + + + Create Task + + +
+
+ + + setFormData({ ...formData, title: e.target.value }) + } + placeholder="Implement feature X" + required + /> +
+
+ +