Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .claude/skills/install-appkit-tarball.md
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's agentic review:

Code Review: Jobs Plugin for Databricks Lakeflow Jobs

Scope: ekniazev/jobs-plugin-core branch, 2 commits, ~39 files changed
Intent: Add a new resource-scoped Jobs plugin for Databricks Lakeflow Jobs with connector, plugin, HTTP routes, SSE streaming, param validation, OBO support, frontend page, docs, and tests. Also fixes StreamManager to abort generators on client disconnect.

P1 -- High

# File Issue Confidence
1 docs/docs/plugins/jobs.md:41 Docs/code mismatch: per-job field name. Docs table says timeout for per-job config, but the TypeScript field in types.ts:69 is waitTimeout. Users following the docs will set the wrong field. 0.95
2 jobs.route.tsx:95-100 SSE stream parsing doesn't handle split chunks. decoder.decode(value, { stream: true }) can split a data: line across two chunks. The code splits on \n and checks line.startsWith("data: "), so a line split mid-chunk would be silently dropped or corrupted. Should buffer incomplete lines between reads. 0.85

P2 -- Moderate

# File Issue Confidence
3 jobs.route.tsx:255-256 React key collision on stream log. key={line} uses the line content as key. Duplicate SSE messages (heartbeats, repeated status like "RUNNING") will produce duplicate keys, causing React reconciliation issues and potential UI glitches. Use index-based key or prepend a counter. 0.90
4 plugin.ts:229-278 Duplicated param validation block. runNow (lines 229-245) and runAndWait (lines 268-283) contain identical validation + mapping logic. If validation rules change, both must be updated. Extract a shared _validateAndMapParams(jobKey, params) method. 0.85
5 connectors/jobs/client.ts:176 Semantic mismatch: ExecutionError.statementFailed. Used for generic Jobs API errors, but the name implies SQL statement failures. Other connectors (sql-warehouse) use it for actual SQL errors. While the error chain works technically, it's misleading for debugging/logging. 0.75

P3 -- Low

# File Issue Confidence
6 plugin.ts:717 Unnecessary wrapper function. ((jobKey: string) => resolveJob(jobKey)) as JobsExport can be simplified to resolveJob as JobsExport. 0.90
7 jobs.route.tsx:23-24 TERMINATED mapped to green unconditionally. stateColor("TERMINATED") returns green, but TERMINATED only means the run ended -- it could have failed. The result_state column handles this separately, but the green lifecycle indicator is misleading when paired with a red result. 0.70
8 package.json (appkit) Zod caret range ^4.3.6. For a published SDK, pinned versions are safer to avoid unexpected breaks from minor Zod releases. The lockfile shows two Zod versions (4.1.13 + 4.3.6) coexisting. 0.65

Coverage

  • Tests: Comprehensive test suite (1712 lines) covering discovery, resource requirements, exports, parameter validation, interceptors, polling, error handling, OBO, route handlers, XSS sanitization.
  • StreamManager fix: Includes a targeted test for the new abort-on-disconnect behavior.
  • Testing gaps: No integration test for the full SSE streaming path (triggerRun -> poll -> client disconnect -> abort). Frontend component has no tests (acceptable for dev-playground).

Verdict: Ready with fixes. The docs/code mismatch (#1) and SSE parsing (#2) should be addressed before merge. Items #3-5 are recommended. Items #6-8 are at your discretion.

Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
name: install-appkit-tarball
description: "Install appkit tgz builds into a project. Downloads from the prepare-release CI workflow (default) or builds locally. Use when testing appkit release candidates, installing pre-release appkit, or when user says 'install appkit tarball', 'install local appkit', 'use local appkit', 'link appkit tgz', 'install appkit from CI'."
user-invocable: true
allowed-tools: Read, Edit, Bash, Glob
---

# Install AppKit Tarball

Installs AppKit tgz packages into a project from either the CI prepare-release workflow or a local build.

## Arguments

Parse the user's input to determine mode and options:

- **No args** → CI mode, latest successful prepare-release run, install in CWD
- **GitHub Actions URL** (contains `actions/runs/`) → CI mode, extract run ID from URL
- **Numeric run ID** → CI mode, that specific run
- **`--local <path>`** → Local build mode from the given appkit repo path
- **`--dir <path>`** (combinable with any above) → Override install target directory (default: CWD)

## Workflow

Follow these steps exactly:

### Step 1: Determine target directory

If `--dir` was provided, use that path. Otherwise use the current working directory.
Verify `package.json` exists in the target directory.

### Step 2: Get tgz files

#### Option A: CI mode (default)

**2a. Find the workflow run:**

If no run ID was provided, get the latest successful run:

```bash
gh run list --repo databricks/appkit --workflow prepare-release.yml --status success --limit 1 --json databaseId,number
```

If a GitHub Actions URL was provided, extract the run ID from it (the number after `/runs/`).

**2b. Find the artifact name:**

```bash
gh api "repos/databricks/appkit/actions/runs/{RUN_ID}/artifacts" --jq '.artifacts[] | select(.name | test("^appkit-release-[0-9]")) | .name'
```

**2c. Download the artifact:**

```bash
gh run download {RUN_ID} --repo databricks/appkit --name "{ARTIFACT_NAME}" --dir /tmp/appkit-release-artifacts
```

**2d. Verify checksums:**

```bash
cd /tmp/appkit-release-artifacts && shasum -a 256 -c SHA256SUMS
```

Note: Use `shasum -a 256` (macOS) not `sha256sum` (Linux).

**2e. Print the downloaded version:**

```bash
cat /tmp/appkit-release-artifacts/VERSION
```

The tgz files are now at `/tmp/appkit-release-artifacts/databricks-appkit-*.tgz` and `/tmp/appkit-release-artifacts/databricks-appkit-ui-*.tgz`.

#### Option B: Local build mode

**2a. Build tgz files:**

```bash
cd {LOCAL_APPKIT_PATH} && pnpm pack:sdk
```

**2b. Find the tgz files:**

Glob for `*.tgz` in:
- `{LOCAL_APPKIT_PATH}/packages/appkit/tmp/*.tgz`
- `{LOCAL_APPKIT_PATH}/packages/appkit-ui/tmp/*.tgz`

There should be exactly one tgz in each directory.

### Step 3: Copy tgz files to target directory

Copy both `databricks-appkit-*.tgz` and `databricks-appkit-ui-*.tgz` to the target directory.

### Step 4: Update package.json

Read `package.json` in the target directory. Edit `@databricks/appkit` and `@databricks/appkit-ui` dependency values to use `file:./` prefix pointing to the tgz filenames.

For example, if the tgz file is `databricks-appkit-0.22.0.tgz`:
```json
"@databricks/appkit": "file:./databricks-appkit-0.22.0.tgz"
```

Same pattern for `@databricks/appkit-ui`.

### Step 5: Install dependencies

Run in the target directory:

```bash
npm install --force ./databricks-appkit-{VERSION}.tgz ./databricks-appkit-ui-{VERSION}.tgz
```

### Step 6: Clean up

If CI mode was used, remove the temp directory:

```bash
rm -rf /tmp/appkit-release-artifacts
```

### Step 7: Report

Print a summary:
- Source: CI run #{number} or local build from {path}
- Version installed
- Target directory
- Installed packages
1 change: 1 addition & 0 deletions apps/dev-playground/.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ OTEL_SERVICE_NAME='dev-playground'
DATABRICKS_VOLUME_PLAYGROUND=
DATABRICKS_VOLUME_OTHER=
DATABRICKS_GENIE_SPACE_ID=
DATABRICKS_JOB_ID=
DATABRICKS_SERVING_ENDPOINT_NAME=
LAKEBASE_ENDPOINT='' # Run: databricks postgres list-endpoints projects/{project-id}/branches/{branch-id} — use the `name` field from the output
PGHOST=
Expand Down
8 changes: 4 additions & 4 deletions apps/dev-playground/client/src/appKitTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ declare module "@databricks/appkit-ui/react" {
result: Array<{
/** @sqlType STRING */
string_value: string;
/** @sqlType STRING */
number_value: string;
/** @sqlType STRING */
boolean_value: string;
/** @sqlType INT */
number_value: number;
/** @sqlType BOOLEAN */
boolean_value: boolean;
/** @sqlType STRING */
date_value: string;
/** @sqlType STRING */
Expand Down
21 changes: 21 additions & 0 deletions apps/dev-playground/client/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Route as SqlHelpersRouteRouteImport } from './routes/sql-helpers.route'
import { Route as ServingRouteRouteImport } from './routes/serving.route'
import { Route as ReconnectRouteRouteImport } from './routes/reconnect.route'
import { Route as LakebaseRouteRouteImport } from './routes/lakebase.route'
import { Route as JobsRouteRouteImport } from './routes/jobs.route'
import { Route as GenieRouteRouteImport } from './routes/genie.route'
import { Route as FilesRouteRouteImport } from './routes/files.route'
import { Route as DataVisualizationRouteRouteImport } from './routes/data-visualization.route'
Expand Down Expand Up @@ -53,6 +54,11 @@ const LakebaseRouteRoute = LakebaseRouteRouteImport.update({
path: '/lakebase',
getParentRoute: () => rootRouteImport,
} as any)
const JobsRouteRoute = JobsRouteRouteImport.update({
id: '/jobs',
path: '/jobs',
getParentRoute: () => rootRouteImport,
} as any)
const GenieRouteRoute = GenieRouteRouteImport.update({
id: '/genie',
path: '/genie',
Expand Down Expand Up @@ -97,6 +103,7 @@ export interface FileRoutesByFullPath {
'/data-visualization': typeof DataVisualizationRouteRoute
'/files': typeof FilesRouteRoute
'/genie': typeof GenieRouteRoute
'/jobs': typeof JobsRouteRoute
'/lakebase': typeof LakebaseRouteRoute
'/reconnect': typeof ReconnectRouteRoute
'/serving': typeof ServingRouteRoute
Expand All @@ -112,6 +119,7 @@ export interface FileRoutesByTo {
'/data-visualization': typeof DataVisualizationRouteRoute
'/files': typeof FilesRouteRoute
'/genie': typeof GenieRouteRoute
'/jobs': typeof JobsRouteRoute
'/lakebase': typeof LakebaseRouteRoute
'/reconnect': typeof ReconnectRouteRoute
'/serving': typeof ServingRouteRoute
Expand All @@ -128,6 +136,7 @@ export interface FileRoutesById {
'/data-visualization': typeof DataVisualizationRouteRoute
'/files': typeof FilesRouteRoute
'/genie': typeof GenieRouteRoute
'/jobs': typeof JobsRouteRoute
'/lakebase': typeof LakebaseRouteRoute
'/reconnect': typeof ReconnectRouteRoute
'/serving': typeof ServingRouteRoute
Expand All @@ -145,6 +154,7 @@ export interface FileRouteTypes {
| '/data-visualization'
| '/files'
| '/genie'
| '/jobs'
| '/lakebase'
| '/reconnect'
| '/serving'
Expand All @@ -160,6 +170,7 @@ export interface FileRouteTypes {
| '/data-visualization'
| '/files'
| '/genie'
| '/jobs'
| '/lakebase'
| '/reconnect'
| '/serving'
Expand All @@ -175,6 +186,7 @@ export interface FileRouteTypes {
| '/data-visualization'
| '/files'
| '/genie'
| '/jobs'
| '/lakebase'
| '/reconnect'
| '/serving'
Expand All @@ -191,6 +203,7 @@ export interface RootRouteChildren {
DataVisualizationRouteRoute: typeof DataVisualizationRouteRoute
FilesRouteRoute: typeof FilesRouteRoute
GenieRouteRoute: typeof GenieRouteRoute
JobsRouteRoute: typeof JobsRouteRoute
LakebaseRouteRoute: typeof LakebaseRouteRoute
ReconnectRouteRoute: typeof ReconnectRouteRoute
ServingRouteRoute: typeof ServingRouteRoute
Expand Down Expand Up @@ -243,6 +256,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LakebaseRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/jobs': {
id: '/jobs'
path: '/jobs'
fullPath: '/jobs'
preLoaderRoute: typeof JobsRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/genie': {
id: '/genie'
path: '/genie'
Expand Down Expand Up @@ -303,6 +323,7 @@ const rootRouteChildren: RootRouteChildren = {
DataVisualizationRouteRoute: DataVisualizationRouteRoute,
FilesRouteRoute: FilesRouteRoute,
GenieRouteRoute: GenieRouteRoute,
JobsRouteRoute: JobsRouteRoute,
LakebaseRouteRoute: LakebaseRouteRoute,
ReconnectRouteRoute: ReconnectRouteRoute,
ServingRouteRoute: ServingRouteRoute,
Expand Down
8 changes: 8 additions & 0 deletions apps/dev-playground/client/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ function RootComponent() {
Files
</Button>
</Link>
<Link to="/jobs" className="no-underline">
<Button
variant="ghost"
className="text-foreground hover:text-secondary-foreground"
>
Jobs
</Button>
</Link>
<Link to="/serving" className="no-underline">
<Button
variant="ghost"
Expand Down
18 changes: 18 additions & 0 deletions apps/dev-playground/client/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,24 @@ function IndexRoute() {
</div>
</Card>

<Card className="p-6 hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex flex-col h-full">
<h3 className="text-2xl font-semibold text-foreground mb-3">
Lakeflow Jobs
</h3>
<p className="text-muted-foreground mb-6 flex-grow">
Trigger and monitor Databricks Lakeflow Jobs. View run history,
stream live status updates, and cancel in-flight runs.
</p>
<Button
onClick={() => navigate({ to: "/jobs" })}
className="w-full"
>
Manage Jobs
</Button>
</div>
</Card>

<Card className="p-6 hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex flex-col h-full">
<h3 className="text-2xl font-semibold text-foreground mb-3">
Expand Down
Loading
Loading