From ec0001ab89767b327ffbcc92962d44923269e1bd Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Wed, 14 Jan 2026 18:07:21 +0100 Subject: [PATCH] chore: add generic template --- biome.json | 3 +- template/.env.example.tmpl | 7 + template/.env.tmpl | 7 + template/.gitignore.tmpl | 10 + template/.prettierignore | 36 ++++ template/.prettierrc.json | 12 ++ template/README.md | 183 ++++++++++++++++++ template/app.yaml.tmpl | 5 + template/client/components.json | 21 ++ template/client/index.html | 18 ++ template/client/postcss.config.js | 6 + template/client/public/apple-touch-icon.png | Bin 0 -> 2547 bytes template/client/public/favicon-16x16.png | Bin 0 -> 302 bytes template/client/public/favicon-192x192.png | Bin 0 -> 2762 bytes template/client/public/favicon-32x32.png | Bin 0 -> 492 bytes template/client/public/favicon-48x48.png | Bin 0 -> 686 bytes template/client/public/favicon-512x512.png | Bin 0 -> 10325 bytes template/client/public/favicon.svg | 6 + template/client/public/site.webmanifest | 19 ++ template/client/src/App.tsx | 155 +++++++++++++++ template/client/src/ErrorBoundary.tsx | 75 +++++++ template/client/src/index.css | 82 ++++++++ template/client/src/lib/utils.ts | 6 + template/client/src/main.tsx | 13 ++ template/client/src/vite-env.d.ts | 1 + template/client/tailwind.config.ts | 10 + template/client/vite.config.ts | 25 +++ template/config/queries/hello_world.sql | 1 + template/config/queries/mocked_sales.sql | 18 ++ template/databricks.yml.tmpl | 36 ++++ template/eslint.config.js | 91 +++++++++ template/features/analytics/app_env.yml | 2 + .../features/analytics/bundle_resources.yml | 4 + .../features/analytics/bundle_variables.yml | 2 + template/features/analytics/dotenv.yml | 1 + .../features/analytics/dotenv_example.yml | 1 + .../features/analytics/target_variables.yml | 1 + template/package.json | 79 ++++++++ template/playwright.config.ts | 26 +++ template/server/server.ts | 8 + template/tests/smoke.spec.ts | 108 +++++++++++ template/tsconfig.client.json | 28 +++ template/tsconfig.json | 4 + template/tsconfig.server.json | 17 ++ template/tsconfig.shared.json | 21 ++ template/vitest.config.ts | 15 ++ vitest.config.ts | 1 + 47 files changed, 1163 insertions(+), 1 deletion(-) create mode 100644 template/.env.example.tmpl create mode 100644 template/.env.tmpl create mode 100644 template/.gitignore.tmpl create mode 100644 template/.prettierignore create mode 100644 template/.prettierrc.json create mode 100644 template/README.md create mode 100644 template/app.yaml.tmpl create mode 100644 template/client/components.json create mode 100644 template/client/index.html create mode 100644 template/client/postcss.config.js create mode 100644 template/client/public/apple-touch-icon.png create mode 100644 template/client/public/favicon-16x16.png create mode 100644 template/client/public/favicon-192x192.png create mode 100644 template/client/public/favicon-32x32.png create mode 100644 template/client/public/favicon-48x48.png create mode 100644 template/client/public/favicon-512x512.png create mode 100644 template/client/public/favicon.svg create mode 100644 template/client/public/site.webmanifest create mode 100644 template/client/src/App.tsx create mode 100644 template/client/src/ErrorBoundary.tsx create mode 100644 template/client/src/index.css create mode 100644 template/client/src/lib/utils.ts create mode 100644 template/client/src/main.tsx create mode 100644 template/client/src/vite-env.d.ts create mode 100644 template/client/tailwind.config.ts create mode 100644 template/client/vite.config.ts create mode 100644 template/config/queries/hello_world.sql create mode 100644 template/config/queries/mocked_sales.sql create mode 100644 template/databricks.yml.tmpl create mode 100644 template/eslint.config.js create mode 100644 template/features/analytics/app_env.yml create mode 100644 template/features/analytics/bundle_resources.yml create mode 100644 template/features/analytics/bundle_variables.yml create mode 100644 template/features/analytics/dotenv.yml create mode 100644 template/features/analytics/dotenv_example.yml create mode 100644 template/features/analytics/target_variables.yml create mode 100644 template/package.json create mode 100644 template/playwright.config.ts create mode 100644 template/server/server.ts create mode 100644 template/tests/smoke.spec.ts create mode 100644 template/tsconfig.client.json create mode 100644 template/tsconfig.json create mode 100644 template/tsconfig.server.json create mode 100644 template/tsconfig.shared.json create mode 100644 template/vitest.config.ts diff --git a/biome.json b/biome.json index 67c39c25..c9b8a4a1 100644 --- a/biome.json +++ b/biome.json @@ -19,7 +19,8 @@ "!docs/.docusaurus", "!**/*.gen.css", "!**/*.gen.ts", - "!**/typedoc-sidebar.ts" + "!**/typedoc-sidebar.ts", + "!**/template" ] }, "formatter": { diff --git a/template/.env.example.tmpl b/template/.env.example.tmpl new file mode 100644 index 00000000..c8b5c441 --- /dev/null +++ b/template/.env.example.tmpl @@ -0,0 +1,7 @@ +DATABRICKS_HOST=https://... +{{- if .dotenv_example}} +{{.dotenv_example}} +{{- end}} +DATABRICKS_APP_PORT=8000 +DATABRICKS_APP_NAME=minimal +FLASK_RUN_HOST=0.0.0.0 diff --git a/template/.env.tmpl b/template/.env.tmpl new file mode 100644 index 00000000..62f55187 --- /dev/null +++ b/template/.env.tmpl @@ -0,0 +1,7 @@ +{{if ne .profile ""}}DATABRICKS_CONFIG_PROFILE={{.profile}}{{else}}DATABRICKS_HOST={{workspace_host}}{{end}} +{{- if .dotenv}} +{{.dotenv}} +{{- end}} +DATABRICKS_APP_PORT=8000 +DATABRICKS_APP_NAME={{.project_name}} +FLASK_RUN_HOST=0.0.0.0 diff --git a/template/.gitignore.tmpl b/template/.gitignore.tmpl new file mode 100644 index 00000000..f2abc326 --- /dev/null +++ b/template/.gitignore.tmpl @@ -0,0 +1,10 @@ +.DS_Store +node_modules/ +client/dist/ +dist/ +build/ +.env +.databricks/ +.smoke-test/ +test-results/ +playwright-report/ diff --git a/template/.prettierignore b/template/.prettierignore new file mode 100644 index 00000000..7d3d77c6 --- /dev/null +++ b/template/.prettierignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules + +# Build outputs +dist +build +client/dist +.next +.databricks/ + +# Environment files +.env +.env.local +.env.*.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Coverage +coverage + +# Cache +.cache +.turbo + +# Lock files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Vendor +vendor diff --git a/template/.prettierrc.json b/template/.prettierrc.json new file mode 100644 index 00000000..d95a63f6 --- /dev/null +++ b/template/.prettierrc.json @@ -0,0 +1,12 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSpacing": true, + "jsxSingleQuote": false +} diff --git a/template/README.md b/template/README.md new file mode 100644 index 00000000..a39fb95c --- /dev/null +++ b/template/README.md @@ -0,0 +1,183 @@ +# Minimal Databricks App + +A minimal Databricks App powered by Databricks AppKit, featuring React, TypeScript, tRPC, and Tailwind CSS. + +## Prerequisites + +- Node.js 18+ and npm +- Databricks CLI (for deployment) +- Access to a Databricks workspace + +## Databricks Authentication + +### Local Development + +For local development, configure your environment variables by creating a `.env` file: + +```bash +cp env.example .env +``` + +Edit `.env` and set the following: + +```env +DATABRICKS_HOST=https://your-workspace.cloud.databricks.com +DATABRICKS_WAREHOUSE_ID=your-warehouse-id +DATABRICKS_APP_PORT=8000 +``` + +### CLI Authentication + +The Databricks CLI requires authentication to deploy and manage apps. Configure authentication using one of these methods: + +#### OAuth U2M + +Interactive browser-based authentication with short-lived tokens: + +```bash +databricks auth login --host https://your-workspace.cloud.databricks.com +``` + +This will open your browser to complete authentication. The CLI saves credentials to `~/.databrickscfg`. + +#### Configuration Profiles + +Use multiple profiles for different workspaces: + +```ini +[DEFAULT] +host = https://dev-workspace.cloud.databricks.com + +[production] +host = https://prod-workspace.cloud.databricks.com +client_id = prod-client-id +client_secret = prod-client-secret +``` + +Deploy using a specific profile: + +```bash +databricks bundle deploy -t prod --profile production +``` + +**Note:** Personal Access Tokens (PATs) are legacy authentication. OAuth is strongly recommended for better security. + +## Getting Started + +### Install Dependencies + +```bash +npm install +``` + +### Development + +Run the app in development mode with hot reload: + +```bash +npm run dev +``` + +The app will be available at the URL shown in the console output. + +### Build + +Build both client and server for production: + +```bash +npm run build +``` + +This creates: + +- `dist/server/` - Compiled server code +- `client/dist/` - Bundled client assets + +### Production + +Run the production build: + +```bash +npm start +``` + +## Code Quality + +```bash +# Type checking +npm run typecheck + +# Linting +npm run lint +npm run lint:fix + +# Formatting +npm run format +npm run format:fix +``` + +## Deployment with Databricks Asset Bundles + +### 1. Configure Bundle + +Update `databricks.yml` with your workspace settings: + +```yaml +targets: + dev: + workspace: + host: https://your-workspace.cloud.databricks.com + variables: + warehouse_id: your-warehouse-id +``` + +### 2. Validate Bundle + +```bash +databricks bundle validate +``` + +### 3. Deploy + +Deploy to the development target: + +```bash +databricks bundle deploy -t dev +``` + +### 4. Run + +Start the deployed app: + +```bash +databricks bundle run -t dev +``` + +### Deploy to Production + +1. Configure the production target in `databricks.yml` +2. Deploy to production: + +```bash +databricks bundle deploy -t prod +``` + +## Project Structure + +``` +* client/ # React frontend + * src/ # Source code + * public/ # Static assets +* server/ # Express backend + * server.ts # Server entry point + * trpc.ts # tRPC router +* shared/ # Shared types +* databricks.yml # Bundle configuration +``` + +## Tech Stack + +- **Frontend**: React 19, TypeScript, Vite, Tailwind CSS +- **Backend**: Node.js, Express, tRPC +- **UI Components**: Radix UI, shadcn/ui +- **Databricks**: App Kit SDK, Analytics SDK diff --git a/template/app.yaml.tmpl b/template/app.yaml.tmpl new file mode 100644 index 00000000..a4e7b27d --- /dev/null +++ b/template/app.yaml.tmpl @@ -0,0 +1,5 @@ +command: ['npm', 'run', 'start'] +{{- if .app_env}} +env: +{{.app_env}} +{{- end}} diff --git a/template/client/components.json b/template/client/components.json new file mode 100644 index 00000000..13e1db0b --- /dev/null +++ b/template/client/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/template/client/index.html b/template/client/index.html new file mode 100644 index 00000000..3c0f326f --- /dev/null +++ b/template/client/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + {{.project_name}} + + +
+ + + diff --git a/template/client/postcss.config.js b/template/client/postcss.config.js new file mode 100644 index 00000000..51a6e4e6 --- /dev/null +++ b/template/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +}; diff --git a/template/client/public/apple-touch-icon.png b/template/client/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..32053bd2d17ca6d4cdcfa2e2390cdc4db61658c1 GIT binary patch literal 2547 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 literal 0 HcmV?d00001 diff --git a/template/client/public/favicon-16x16.png b/template/client/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..d7c16eb471f63a06f45119cebeb085dcc6ce3252 GIT binary patch literal 302 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 Uz+N|&sQ>@~07*qoM6N<$f;65h(*OVf literal 0 HcmV?d00001 diff --git a/template/client/public/favicon-512x512.png b/template/client/public/favicon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..14d7d209db562940b4e78d23fce203d69afe7c38 GIT binary patch literal 10325 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 boicPbM8)qSin-XseaGC)+VuG$w_E=Y^dlWB literal 0 HcmV?d00001 diff --git a/template/client/public/favicon.svg b/template/client/public/favicon.svg new file mode 100644 index 00000000..cb30c1e0 --- /dev/null +++ b/template/client/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/template/client/public/site.webmanifest b/template/client/public/site.webmanifest new file mode 100644 index 00000000..03106ced --- /dev/null +++ b/template/client/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "{{.project_name}}", + "short_name": "{{.project_name}}", + "icons": [ + { + "src": "/favicon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/template/client/src/App.tsx b/template/client/src/App.tsx new file mode 100644 index 00000000..12cd542a --- /dev/null +++ b/template/client/src/App.tsx @@ -0,0 +1,155 @@ +import { + useAnalyticsQuery, + AreaChart, + LineChart, + RadarChart, + Card, + CardContent, + CardHeader, + CardTitle, + Skeleton, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@databricks/appkit-ui/react'; +import { sql } from "@databricks/appkit-ui/js"; +import { useState, useEffect } from 'react'; + +function App() { + const { data, loading, error } = useAnalyticsQuery('hello_world', { + message: sql.string('hello world'), + }); + + const [health, setHealth] = useState<{ + status: string; + timestamp: string; + } | null>(null); + const [healthError, setHealthError] = useState(null); + + useEffect(() => { + fetch('/health') + .then((response) => response.json()) + .then((data: { status: string }) => setHealth({ ...data, timestamp: new Date().toISOString() })) + .catch((error: { message: string }) => setHealthError(error.message)); + }, []); + + const [maxMonthNum, setMaxMonthNum] = useState(12); + + const salesParameters = { max_month_num: sql.number(maxMonthNum) }; + + return ( +
+
+

Minimal Databricks App

+

A minimal Databricks App powered by Databricks AppKit

+
+ +
+ + + SQL Query Result + + + {loading && ( +
+ + +
+ )} + {error &&
Error: {error}
} + {data && data.length > 0 && ( +
+
Query: SELECT :message AS value
+
{data[0].value}
+
+ )} + {data && data.length === 0 &&
No results
} +
+
+ + + + Health Check + + + {!health && !healthError && ( +
+ + +
+ )} + {healthError && ( +
Error: {healthError}
+ )} + {health && ( +
+
+
+
{health.status.toUpperCase()}
+
+
+ Last checked: {new Date(health.timestamp).toLocaleString()} +
+
+ )} +
+
+ + + + Sales Data Filter + + +
+
+ + +
+
+
+
+ + + + Sales Trend Area Chart + + + + + + + + Sales Trend Custom Line Chart + + + + + + + + Sales Trend Radar Chart + + + + + +
+
+ ); +} + +export default App; diff --git a/template/client/src/ErrorBoundary.tsx b/template/client/src/ErrorBoundary.tsx new file mode 100644 index 00000000..6a73c26c --- /dev/null +++ b/template/client/src/ErrorBoundary.tsx @@ -0,0 +1,75 @@ +import React, { Component } from 'react'; +import type { ReactNode } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@databricks/appkit-ui/react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught an error:', error); + console.error('Error details:', errorInfo); + this.setState({ + error, + errorInfo, + }); + } + + render() { + if (this.state.hasError) { + return ( +
+ + + Application Error + + +
+
+

Error Message:

+
{this.state.error?.toString()}
+
+ {this.state.errorInfo && ( +
+

Component Stack:

+
+                      {this.state.errorInfo.componentStack}
+                    
+
+ )} + {this.state.error?.stack && ( +
+

Stack Trace:

+
{this.state.error.stack}
+
+ )} +
+
+
+
+ ); + } + + return this.props.children; + } +} diff --git a/template/client/src/index.css b/template/client/src/index.css new file mode 100644 index 00000000..0ce57a7d --- /dev/null +++ b/template/client/src/index.css @@ -0,0 +1,82 @@ +@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); */ + } +} diff --git a/template/client/src/lib/utils.ts b/template/client/src/lib/utils.ts new file mode 100644 index 00000000..2819a830 --- /dev/null +++ b/template/client/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/template/client/src/main.tsx b/template/client/src/main.tsx new file mode 100644 index 00000000..35c59a58 --- /dev/null +++ b/template/client/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App.tsx'; +import { ErrorBoundary } from './ErrorBoundary.tsx'; + +createRoot(document.getElementById('root')!).render( + + + + + +); diff --git a/template/client/src/vite-env.d.ts b/template/client/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/template/client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/template/client/tailwind.config.ts b/template/client/tailwind.config.ts new file mode 100644 index 00000000..31dece86 --- /dev/null +++ b/template/client/tailwind.config.ts @@ -0,0 +1,10 @@ +import type { Config } from 'tailwindcss'; +import tailwindcssAnimate from 'tailwindcss-animate'; + +const config: Config = { + darkMode: ['class', 'media'], + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + plugins: [tailwindcssAnimate], +}; + +export default config; diff --git a/template/client/vite.config.ts b/template/client/vite.config.ts new file mode 100644 index 00000000..b49d4055 --- /dev/null +++ b/template/client/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import path from 'node:path'; + +// https://vite.dev/config/ +export default defineConfig({ + root: __dirname, + plugins: [react(), tailwindcss()], + server: { + middlewareMode: true, + }, + build: { + outDir: path.resolve(__dirname, './dist'), + emptyOutDir: true, + }, + optimizeDeps: { + include: ['react', 'react-dom', 'react/jsx-dev-runtime', 'react/jsx-runtime', 'recharts'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); diff --git a/template/config/queries/hello_world.sql b/template/config/queries/hello_world.sql new file mode 100644 index 00000000..31e480f7 --- /dev/null +++ b/template/config/queries/hello_world.sql @@ -0,0 +1 @@ +SELECT :message AS value; diff --git a/template/config/queries/mocked_sales.sql b/template/config/queries/mocked_sales.sql new file mode 100644 index 00000000..868e9472 --- /dev/null +++ b/template/config/queries/mocked_sales.sql @@ -0,0 +1,18 @@ +WITH mock_data AS ( + SELECT 'January' AS month, 1 AS month_num, 65000 AS revenue, 45000 AS expenses, 850 AS customers + UNION ALL SELECT 'February', 2, 72000, 48000, 920 + UNION ALL SELECT 'March', 3, 78000, 52000, 1050 + UNION ALL SELECT 'April', 4, 85000, 55000, 1180 + UNION ALL SELECT 'May', 5, 92000, 58000, 1320 + UNION ALL SELECT 'June', 6, 88000, 54000, 1250 + UNION ALL SELECT 'July', 7, 95000, 60000, 1380 + UNION ALL SELECT 'August', 8, 89000, 56000, 1290 + UNION ALL SELECT 'September', 9, 82000, 53000, 1150 + UNION ALL SELECT 'October', 10, 87000, 55000, 1220 + UNION ALL SELECT 'November', 11, 93000, 59000, 1340 + UNION ALL SELECT 'December', 12, 98000, 62000, 1420 +) +SELECT * +FROM mock_data +WHERE month_num <= :max_month_num +ORDER BY month_num; diff --git a/template/databricks.yml.tmpl b/template/databricks.yml.tmpl new file mode 100644 index 00000000..cdfa3fe0 --- /dev/null +++ b/template/databricks.yml.tmpl @@ -0,0 +1,36 @@ +bundle: + name: {{.project_name}} +{{if .bundle_variables}} + +variables: +{{.bundle_variables}} +{{- end}} + +resources: + apps: + app: + name: "{{.project_name}}" + description: "{{.app_description}}" + source_code_path: ./ + + # Uncomment to enable on behalf of user API scopes. Available scopes: sql, dashboards.genie, files.files + # user_api_scopes: + # - sql +{{if .bundle_resources}} + + # The resources which this app has access to. + resources: +{{.bundle_resources}} +{{- end}} + +targets: + default: + # mode: production + default: true + workspace: + host: {{workspace_host}} +{{if .target_variables}} + + variables: +{{.target_variables}} +{{- end}} diff --git a/template/eslint.config.js b/template/eslint.config.js new file mode 100644 index 00000000..5ac5ece8 --- /dev/null +++ b/template/eslint.config.js @@ -0,0 +1,91 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; +import reactRefreshPlugin from 'eslint-plugin-react-refresh'; +import prettier from 'eslint-config-prettier'; + +export default tseslint.config( + // Global ignores + { + ignores: [ + '**/dist/**', + '**/build/**', + '**/node_modules/**', + '**/.next/**', + '**/coverage/**', + 'client/dist/**', + '**.databricks/**', + 'tests/**', + '**/.smoke-test/**', + ], + }, + + // Base JavaScript config + js.configs.recommended, + + // TypeScript config for all TS files + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + + // React config for client-side files + { + files: ['client/**/*.{ts,tsx}', '**/*.tsx'], + plugins: { + react: reactPlugin, + 'react-hooks': reactHooksPlugin, + 'react-refresh': reactRefreshPlugin, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + ...reactPlugin.configs.recommended.rules, + ...reactPlugin.configs['jsx-runtime'].rules, + ...reactHooksPlugin.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + 'react/prop-types': 'off', // Using TypeScript for prop validation + 'react/no-array-index-key': 'warn', + }, + }, + + // Node.js specific config for server files + { + files: ['server/**/*.ts', '*.config.{js,ts}'], + rules: { + '@typescript-eslint/no-var-requires': 'off', + }, + }, + + // Disable type-checking for JS config files and standalone config files + { + files: ['**/*.js', '*.config.ts', '**/*.config.ts'], + ...tseslint.configs.disableTypeChecked, + }, + + // Prettier config (must be last to override other formatting rules) + prettier, + + // Custom rules + { + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + }, + } +); diff --git a/template/features/analytics/app_env.yml b/template/features/analytics/app_env.yml new file mode 100644 index 00000000..9228a9dd --- /dev/null +++ b/template/features/analytics/app_env.yml @@ -0,0 +1,2 @@ + - name: DATABRICKS_WAREHOUSE_ID + valueFrom: warehouse diff --git a/template/features/analytics/bundle_resources.yml b/template/features/analytics/bundle_resources.yml new file mode 100644 index 00000000..b3a631c0 --- /dev/null +++ b/template/features/analytics/bundle_resources.yml @@ -0,0 +1,4 @@ + - name: 'warehouse' + sql_warehouse: + id: ${var.warehouse_id} + permission: 'CAN_USE' diff --git a/template/features/analytics/bundle_variables.yml b/template/features/analytics/bundle_variables.yml new file mode 100644 index 00000000..ac4fbf15 --- /dev/null +++ b/template/features/analytics/bundle_variables.yml @@ -0,0 +1,2 @@ + warehouse_id: + description: The ID of the warehouse to use diff --git a/template/features/analytics/dotenv.yml b/template/features/analytics/dotenv.yml new file mode 100644 index 00000000..7d17f13c --- /dev/null +++ b/template/features/analytics/dotenv.yml @@ -0,0 +1 @@ +DATABRICKS_WAREHOUSE_ID={{.sql_warehouse_id}} diff --git a/template/features/analytics/dotenv_example.yml b/template/features/analytics/dotenv_example.yml new file mode 100644 index 00000000..1ae1aa74 --- /dev/null +++ b/template/features/analytics/dotenv_example.yml @@ -0,0 +1 @@ +DATABRICKS_WAREHOUSE_ID= diff --git a/template/features/analytics/target_variables.yml b/template/features/analytics/target_variables.yml new file mode 100644 index 00000000..0de7b63b --- /dev/null +++ b/template/features/analytics/target_variables.yml @@ -0,0 +1 @@ + warehouse_id: {{.sql_warehouse_id}} diff --git a/template/package.json b/template/package.json new file mode 100644 index 00000000..f3f9d4f0 --- /dev/null +++ b/template/package.json @@ -0,0 +1,79 @@ +{ + "name": "{{.project_name}}", + "version": "1.0.0", + "main": "build/index.js", + "type": "module", + "scripts": { + "start": "NODE_ENV=production node --env-file-if-exists=./.env ./dist/server/server.js", + "dev": "NODE_ENV=development tsx watch --tsconfig ./tsconfig.server.json --env-file-if-exists=./.env ./server/server.ts", + "build:client": "tsc -b tsconfig.client.json && vite build --config client/vite.config.ts", + "build:server": "tsc -b tsconfig.server.json", + "build": "npm run build:server && npm run build:client", + "typecheck": "tsc -p ./tsconfig.server.json --noEmit && tsc -p ./tsconfig.client.json --noEmit", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "lint:ast-grep": "appkit-lint", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "test": "vitest run && npm run test:smoke", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:smoke": "playwright install chromium && playwright test tests/smoke.spec.ts", + "clean": "rm -rf client/dist dist build node_modules .smoke-test test-results playwright-report", + "typegen": "appkit-generate-types", + "setup": "appkit-setup --write" + }, + "keywords": [], + "author": "", + "license": "Unlicensed", + "description": "{{.app_description}}", + "dependencies": { + "@databricks/appkit": "0.1.4", + "@databricks/appkit-ui": "0.1.4", + "@databricks/sdk-experimental": "^0.14.2", + "clsx": "^2.1.1", + "embla-carousel-react": "^8.6.0", + "lucide-react": "^0.546.0", + "next-themes": "^0.4.6", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-resizable-panels": "^3.0.6", + "superjson": "^2.2.5", + "tailwind-merge": "^3.3.1", + "tw-animate-css": "^1.4.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^4.1.13" + }, + "devDependencies": { + "@ast-grep/napi": "^0.37.0", + "@eslint/compat": "^2.0.0", + "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", + "@tailwindcss/postcss": "^4.1.17", + "@tailwindcss/vite": "^4.1.17", + "@types/express": "^5.0.5", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@typescript-eslint/eslint-plugin": "^8.48.0", + "@typescript-eslint/parser": "^8.48.0", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "prettier": "^3.6.2", + "sharp": "^0.34.5", + "tailwindcss": "^4.0.14", + "tsx": "^4.20.6", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "npm:rolldown-vite@7.1.14", + "vitest": "^4.0.14" + }, + "overrides": { + "vite": "npm:rolldown-vite@7.1.14" + } +} diff --git a/template/playwright.config.ts b/template/playwright.config.ts new file mode 100644 index 00000000..c4cad7a5 --- /dev/null +++ b/template/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: `http://localhost:${process.env.PORT || 8000}`, + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npm run dev', + url: `http://localhost:${process.env.PORT || 8000}`, + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/template/server/server.ts b/template/server/server.ts new file mode 100644 index 00000000..da041927 --- /dev/null +++ b/template/server/server.ts @@ -0,0 +1,8 @@ +import { createApp, server, {{.plugin_import}} } from '@databricks/appkit'; + +createApp({ + plugins: [ + server(), + {{.plugin_usage}}, + ], +}).catch(console.error); diff --git a/template/tests/smoke.spec.ts b/template/tests/smoke.spec.ts new file mode 100644 index 00000000..d712c695 --- /dev/null +++ b/template/tests/smoke.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from '@playwright/test'; +import { writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +let testArtifactsDir: string; +let consoleLogs: string[] = []; +let consoleErrors: string[] = []; +let pageErrors: string[] = []; +let failedRequests: string[] = []; + +test('smoke test - app loads and displays data', async ({ page }) => { + // Navigate to the app + await page.goto('/'); + + // ⚠️ UPDATE THESE SELECTORS after customizing App.tsx: + // - Change heading name to match your app title + // - Change data selector to match your primary data display + await expect(page.getByRole('heading', { name: 'Minimal Databricks App' })).toBeVisible(); + await expect(page.getByText('hello world', { exact: true })).toBeVisible({ timeout: 30000 }); + + // Wait for health check to complete (wait for "OK" status) + await expect(page.getByText('OK')).toBeVisible({ timeout: 30000 }); + + // Verify console logs were captured + expect(consoleLogs.length).toBeGreaterThan(0); + expect(consoleErrors.length).toBe(0); + expect(pageErrors.length).toBe(0); +}); + +test.beforeEach(async ({ page }) => { + consoleLogs = []; + consoleErrors = []; + pageErrors = []; + failedRequests = []; + + // Create temp directory for test artifacts + testArtifactsDir = join(process.cwd(), '.smoke-test'); + mkdirSync(testArtifactsDir, { recursive: true }); + + // Capture console logs and errors (including React errors) + page.on('console', (msg) => { + const type = msg.type(); + const text = msg.text(); + + // Skip empty lines and formatting placeholders + if (!text.trim() || /^%[osd]$/.test(text.trim())) { + return; + } + + // Get stack trace for errors if available + const location = msg.location(); + const locationStr = location.url ? ` at ${location.url}:${location.lineNumber}:${location.columnNumber}` : ''; + + consoleLogs.push(`[${type}] ${text}${locationStr}`); + + // Separately track error messages (React errors appear here) + if (type === 'error') { + consoleErrors.push(`${text}${locationStr}`); + } + }); + + // Capture page errors with full stack trace + page.on('pageerror', (error) => { + const errorDetails = `Page error: ${error.message}\nStack: ${error.stack || 'No stack trace available'}`; + pageErrors.push(errorDetails); + // Also log to console for immediate visibility + console.error('Page error detected:', errorDetails); + }); + + // Capture failed requests + page.on('requestfailed', (request) => { + failedRequests.push(`Failed request: ${request.url()} - ${request.failure()?.errorText}`); + }); +}); + +test.afterEach(async ({ page }, testInfo) => { + const testName = testInfo.title.replace(/ /g, '-').toLowerCase(); + // Always capture artifacts, even if test fails + const screenshotPath = join(testArtifactsDir, `${testName}-app-screenshot.png`); + await page.screenshot({ path: screenshotPath, fullPage: true }); + + const logsPath = join(testArtifactsDir, `${testName}-console-logs.txt`); + const allLogs = [ + '=== Console Logs ===', + ...consoleLogs, + '\n=== Console Errors (React errors) ===', + ...consoleErrors, + '\n=== Page Errors ===', + ...pageErrors, + '\n=== Failed Requests ===', + ...failedRequests, + ]; + writeFileSync(logsPath, allLogs.join('\n'), 'utf-8'); + + console.log(`Screenshot saved to: ${screenshotPath}`); + console.log(`Console logs saved to: ${logsPath}`); + if (consoleErrors.length > 0) { + console.log('Console errors detected:', consoleErrors); + } + if (pageErrors.length > 0) { + console.log('Page errors detected:', pageErrors); + } + if (failedRequests.length > 0) { + console.log('Failed requests detected:', failedRequests); + } + + await page.close(); +}); diff --git a/template/tsconfig.client.json b/template/tsconfig.client.json new file mode 100644 index 00000000..8732ba46 --- /dev/null +++ b/template/tsconfig.client.json @@ -0,0 +1,28 @@ +{ + "extends": "./tsconfig.shared.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.client.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + + /* Bundler mode */ + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "erasableSyntaxOnly": true, + "noUncheckedSideEffectImports": true, + + /* Paths */ + "baseUrl": "./client", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["client/src"] +} diff --git a/template/tsconfig.json b/template/tsconfig.json new file mode 100644 index 00000000..a51fbad9 --- /dev/null +++ b/template/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.client.json" }, { "path": "./tsconfig.server.json" }] +} diff --git a/template/tsconfig.server.json b/template/tsconfig.server.json new file mode 100644 index 00000000..8cdada22 --- /dev/null +++ b/template/tsconfig.server.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.shared.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.server.tsbuildinfo", + "target": "ES2020", + "lib": ["ES2020"], + + /* Emit */ + "outDir": "./dist", + "rootDir": "./", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["server/**/*", "shared/**/*", "config/**/*"], + "exclude": ["node_modules", "dist", "client"] +} diff --git a/template/tsconfig.shared.json b/template/tsconfig.shared.json new file mode 100644 index 00000000..3187705b --- /dev/null +++ b/template/tsconfig.shared.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + /* Type Checking */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + + /* Modules */ + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + + /* Emit */ + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/template/vitest.config.ts b/template/vitest.config.ts new file mode 100644 index 00000000..98134fd5 --- /dev/null +++ b/template/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import path from 'node:path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + exclude: ['**/node_modules/**', '**/dist/**', '**/*.spec.ts', '**/.smoke-test/**', '**/.databricks/**'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './client/src'), + }, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts index 1238e62f..2e612f1b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ "**/*.config.*", "**/*.test.*", "**/tests/**", + "**/template/**", ], }, projects: [