├── .gitmodules ├── .nvmrc ├── provisioning ├── datasources │ ├── .gitkeep │ └── datasources.yml ├── README.md └── dashboards │ ├── default.yml │ └── variables.json ├── .cprc.json ├── .config ├── .cprc.json ├── webpack │ ├── constants.ts │ ├── utils.ts │ └── webpack.config.ts ├── .prettierrc.js ├── entrypoint.sh ├── .eslintrc ├── types │ └── custom.d.ts ├── tsconfig.json ├── jest-setup.js ├── jest │ ├── mocks │ │ └── react-inlinesvg.tsx │ └── utils.js ├── jest.config.js ├── supervisord │ └── supervisord.conf ├── Dockerfile └── README.md ├── .eslintrc ├── tsconfig.json ├── jest-setup.js ├── ui-example.png ├── .prettierrc.js ├── jest.config.js ├── .vscode └── launch.json ├── Magefile.go ├── src ├── components │ ├── QueryEditor.tsx │ └── ConfigEditor.tsx ├── module.ts ├── plugin.json ├── types.ts ├── queryDefaults.ts ├── img │ └── logo.svg ├── README.md ├── sqlUtil.ts └── datasource.ts ├── CHANGELOG.md ├── .github └── workflows │ ├── is-compatible.yml │ ├── release.yml │ └── ci.yml ├── .gitignore ├── docker-compose.yaml ├── pkg ├── models │ └── settings.go ├── main.go └── plugin │ ├── datasource_test.go │ ├── datasource.go │ └── duckdb_driver.go ├── tests ├── queryVariables.spec.ts ├── queryEditor.spec.ts └── configEditor.spec.ts ├── cmd └── preinstall-extensions │ ├── go.mod │ ├── main.go │ └── go.sum ├── CONTRIBUTING.md ├── playwright.config.ts ├── package.json ├── go.mod ├── README.md ├── LICENSE └── go.sum /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /provisioning/datasources/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": {} 3 | } 4 | -------------------------------------------------------------------------------- /.config/.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.6.1" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/.eslintrc" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | // Jest setup provided by Grafana scaffolding 2 | import './.config/jest-setup'; 3 | -------------------------------------------------------------------------------- /.config/webpack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_DIR = 'src'; 2 | export const DIST_DIR = 'dist'; 3 | -------------------------------------------------------------------------------- /ui-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motherduckdb/grafana-duckdb-datasource/HEAD/ui-example.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Prettier configuration provided by Grafana scaffolding 3 | ...require('./.config/.prettierrc.js'), 4 | }; 5 | -------------------------------------------------------------------------------- /provisioning/README.md: -------------------------------------------------------------------------------- 1 | For more information see [Provision dashboards and data sources](https://grafana.com/tutorials/provision-dashboards-and-data-sources/) 2 | -------------------------------------------------------------------------------- /provisioning/dashboards/default.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: Test provider 5 | type: file 6 | options: 7 | path: 8 | /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // force timezone to UTC to allow tests to work regardless of local timezone 2 | // generally used by snapshots, but can affect specific tests 3 | process.env.TZ = 'UTC'; 4 | 5 | module.exports = { 6 | // Jest configuration provided by Grafana scaffolding 7 | ...require('./.config/jest.config'), 8 | }; 9 | -------------------------------------------------------------------------------- /provisioning/datasources/datasources.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: 'inmemory-duckdb-datasource' 5 | uid: 42 6 | type: 'motherduck-duckdb-datasource' 7 | access: proxy 8 | isDefault: false 9 | orgId: 1 10 | version: 1 11 | editable: true 12 | jsonData: 13 | path: '' 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Standalone debug mode", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "debug", 9 | "program": "${workspaceFolder}/pkg", 10 | "env": {}, 11 | "args": ["-standalone"] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | package main 5 | 6 | import ( 7 | // mage:import 8 | build "github.com/grafana/grafana-plugin-sdk-go/build" 9 | ) 10 | 11 | var Default = build.BuildAll 12 | 13 | var _ = build.SetBeforeBuildCallback(func(cfg build.Config) (build.Config, error) { 14 | cfg.EnableCGo = true 15 | return cfg, nil 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/QueryEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { QueryEditorProps } from '@grafana/data'; 3 | import { DuckDBDataSource } from '../datasource'; 4 | import { SqlQueryEditor, SQLQuery, SQLOptions } from '@grafana/plugin-ui'; 5 | 6 | export function DuckDBQueryEditor(props: QueryEditorProps) { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /.config/.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | module.exports = { 8 | endOfLine: 'auto', 9 | printWidth: 120, 10 | trailingComma: 'es5', 11 | semi: true, 12 | jsxSingleQuote: false, 13 | singleQuote: true, 14 | useTabs: false, 15 | tabWidth: 2, 16 | }; 17 | -------------------------------------------------------------------------------- /.config/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "${DEV}" = "false" ]; then 4 | echo "Starting test mode" 5 | exec /run.sh 6 | fi 7 | 8 | echo "Starting development mode" 9 | 10 | if grep -i -q alpine /etc/issue; then 11 | exec /usr/bin/supervisord -c /etc/supervisord.conf 12 | elif grep -i -q ubuntu /etc/issue; then 13 | exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf 14 | else 15 | echo 'ERROR: Unsupported base image' 16 | exit 1 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## 0.2.1 5 | - Fix multiple query variables. 6 | - Build the plugin for intel mac. 7 | 8 | ## 0.2.0 9 | 10 | - Bump duckdb to 1.2.2. 11 | - Remove the dependency on the forked grafana-plugin-sdk-go. 12 | - Avoid setting motherduck token as environment variable, use the duckdb config instead. 13 | 14 | ## 0.1.1 15 | Changes default duckdb_directory to $GF_PATHS_DATA when the variable is set, so there's no permission issues with the Grafana (Ubuntu) docker image out of the box. 16 | 17 | 18 | ## 0.1.0 19 | 20 | Initial release. 21 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { DuckDBDataSource } from './datasource'; 3 | import { ConfigEditor } from './components/ConfigEditor'; 4 | import { DuckDBQueryEditor } from './components/QueryEditor'; 5 | import { DuckDBDataSourceOptions, SecureJsonData } from './types'; 6 | import { SQLQuery } from '@grafana/plugin-ui'; 7 | 8 | export const plugin = new DataSourcePlugin(DuckDBDataSource) 9 | .setConfigEditor(ConfigEditor) 10 | .setQueryEditor(DuckDBQueryEditor); 11 | -------------------------------------------------------------------------------- /.github/workflows/is-compatible.yml: -------------------------------------------------------------------------------- 1 | name: Latest Grafana API compatibility check 2 | on: [pull_request] 3 | 4 | jobs: 5 | compatibilitycheck: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Setup Node.js environment 10 | uses: actions/setup-node@v4 11 | with: 12 | node-version: '20' 13 | cache: 'npm' 14 | - name: Install dependencies 15 | run: npm ci 16 | - name: Build plugin 17 | run: npm run build 18 | - name: Compatibility check 19 | run: npx @grafana/levitate@latest is-compatible --path src/module.ts --target @grafana/data,@grafana/ui,@grafana/runtime 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .pnpm-debug.log* 8 | 9 | node_modules/ 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Compiled binary addons (https://nodejs.org/api/addons.html) 24 | dist/ 25 | artifacts/ 26 | work/ 27 | ci/ 28 | 29 | # e2e test directories 30 | /test-results/ 31 | /playwright-report/ 32 | /blob-report/ 33 | /playwright/.cache/ 34 | /playwright/.auth/ 35 | 36 | # Editor 37 | .idea 38 | 39 | .eslintcache 40 | .bra.toml -------------------------------------------------------------------------------- /.config/.eslintrc: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config 6 | */ 7 | { 8 | "extends": ["@grafana/eslint-config"], 9 | "root": true, 10 | "rules": { 11 | "react/prop-types": "off" 12 | }, 13 | "overrides": [ 14 | { 15 | "plugins": ["deprecation"], 16 | "files": ["src/**/*.{ts,tsx}"], 17 | "rules": { 18 | "deprecation/deprecation": "warn" 19 | }, 20 | "parserOptions": { 21 | "project": "./tsconfig.json" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.config/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | // Image declarations 2 | declare module '*.gif' { 3 | const src: string; 4 | export default src; 5 | } 6 | 7 | declare module '*.jpg' { 8 | const src: string; 9 | export default src; 10 | } 11 | 12 | declare module '*.jpeg' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.png' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.webp' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.svg' { 28 | const content: string; 29 | export default content; 30 | } 31 | 32 | // Font declarations 33 | declare module '*.woff'; 34 | declare module '*.woff2'; 35 | declare module '*.eot'; 36 | declare module '*.ttf'; 37 | declare module '*.otf'; 38 | -------------------------------------------------------------------------------- /.config/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config 6 | */ 7 | { 8 | "compilerOptions": { 9 | "alwaysStrict": true, 10 | "declaration": false, 11 | "rootDir": "../src", 12 | "baseUrl": "../src", 13 | "typeRoots": ["../node_modules/@types"], 14 | "resolveJsonModule": true 15 | }, 16 | "ts-node": { 17 | "compilerOptions": { 18 | "module": "commonjs", 19 | "target": "es5", 20 | "esModuleInterop": true 21 | }, 22 | "transpileOnly": true 23 | }, 24 | "include": ["../src", "./types"], 25 | "extends": "@grafana/tsconfig" 26 | } 27 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", 3 | "type": "datasource", 4 | "name": "Duckdb-Datasource", 5 | "id": "motherduck-duckdb-datasource", 6 | "metrics": true, 7 | "backend": true, 8 | "alerting": true, 9 | "executable": "gpx_duckdb_datasource", 10 | "info": { 11 | "description": "DuckDB and MotherDuck Data source for Grafana", 12 | "author": { 13 | "name": "MotherDuck" 14 | }, 15 | "keywords": ["datasource"], 16 | "logos": { 17 | "small": "img/logo.svg", 18 | "large": "img/logo.svg" 19 | }, 20 | "links": [], 21 | "screenshots": [], 22 | "version": "%VERSION%", 23 | "updated": "%TODAY%" 24 | }, 25 | "dependencies": { 26 | "grafanaDependency": ">=10.4.0", 27 | "plugins": [] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { SQLOptions } from '@grafana/plugin-ui'; 2 | 3 | 4 | // export interface DuckDBQuery extends SQLQuery { 5 | // queryText?: string; 6 | // constant: number; 7 | // } 8 | 9 | // export const DEFAULT_QUERY: Partial = { 10 | // constant: 6.5, 11 | // }; 12 | 13 | // export interface DataPoint { 14 | // Time: number; 15 | // Value: number; 16 | // } 17 | 18 | // export interface DataSourceResponse { 19 | // datapoints: DataPoint[]; 20 | // } 21 | 22 | /** 23 | * These are options configured for each DataSource instance 24 | */ 25 | export interface DuckDBDataSourceOptions extends SQLOptions { 26 | path?: string; 27 | initSql?: string; 28 | } 29 | 30 | /** 31 | * Value that is used in the backend, but never sent over HTTP to the frontend 32 | */ 33 | export interface SecureJsonData { 34 | motherDuckToken?: string; 35 | } 36 | -------------------------------------------------------------------------------- /.config/jest-setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | import '@testing-library/jest-dom'; 9 | 10 | // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom 11 | Object.defineProperty(global, 'matchMedia', { 12 | writable: true, 13 | value: jest.fn().mockImplementation((query) => ({ 14 | matches: false, 15 | media: query, 16 | onchange: null, 17 | addListener: jest.fn(), // deprecated 18 | removeListener: jest.fn(), // deprecated 19 | addEventListener: jest.fn(), 20 | removeEventListener: jest.fn(), 21 | dispatchEvent: jest.fn(), 22 | })), 23 | }); 24 | 25 | HTMLCanvasElement.prototype.getContext = () => {}; 26 | -------------------------------------------------------------------------------- /.config/jest/mocks/react-inlinesvg.tsx: -------------------------------------------------------------------------------- 1 | // Due to the grafana/ui Icon component making fetch requests to 2 | // `/public/img/icon/.svg` we need to mock react-inlinesvg to prevent 3 | // the failed fetch requests from displaying errors in console. 4 | 5 | import React from 'react'; 6 | 7 | type Callback = (...args: any[]) => void; 8 | 9 | export interface StorageItem { 10 | content: string; 11 | queue: Callback[]; 12 | status: string; 13 | } 14 | 15 | export const cacheStore: { [key: string]: StorageItem } = Object.create(null); 16 | 17 | const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/; 18 | 19 | const InlineSVG = ({ src }: { src: string }) => { 20 | // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`) 21 | const testId = src.replace(SVG_FILE_NAME_REGEX, '$2'); 22 | return ; 23 | }; 24 | 25 | export default InlineSVG; 26 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | 3 | services: 4 | grafana: 5 | user: root 6 | container_name: 'motherduck-duckdb-datasource' 7 | 8 | build: 9 | context: ./.config 10 | args: 11 | grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} 12 | grafana_version: ${GRAFANA_VERSION:-11.6.0} 13 | ports: 14 | - 3000:3000/tcp 15 | - 2345:2345/tcp # delve 16 | security_opt: 17 | - 'apparmor:unconfined' 18 | - 'seccomp:unconfined' 19 | cap_add: 20 | - SYS_PTRACE 21 | volumes: 22 | - ./dist:/var/lib/grafana/plugins/motherduck-duckdb-datasource 23 | - ./provisioning:/etc/grafana/provisioning 24 | - .:/root/motherduck-duckdb-datasource 25 | 26 | environment: 27 | NODE_ENV: development 28 | GF_LOG_FILTERS: plugin.motherduck-duckdb-datasource:debug 29 | GF_LOG_LEVEL: info 30 | GF_DATAPROXY_LOGGING: 1 31 | GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: motherduck-duckdb-datasource 32 | -------------------------------------------------------------------------------- /src/queryDefaults.ts: -------------------------------------------------------------------------------- 1 | import { SQLQuery, QueryFormat, EditorMode } from '@grafana/plugin-ui'; 2 | import { createFunctionField, setGroupByField } from './sqlUtil'; 3 | 4 | 5 | export function applyQueryDefaults(q?: SQLQuery): SQLQuery { 6 | let editorMode = q?.editorMode || EditorMode.Builder; 7 | 8 | // Switching to code editor if the query was created before visual query builder was introduced. 9 | if (q?.editorMode === undefined && q?.rawSql !== undefined) { 10 | editorMode = EditorMode.Code; 11 | } 12 | 13 | const result: SQLQuery = { 14 | ...q, 15 | refId: q?.refId || 'A', 16 | format: q?.format !== undefined ? q.format : QueryFormat.Table, 17 | rawSql: q?.rawSql || 'select 42', 18 | editorMode, 19 | dataset: "default", 20 | sql: q?.sql || { 21 | columns: [createFunctionField()], 22 | groupBy: [setGroupByField()], 23 | limit: 50, 24 | }, 25 | }; 26 | 27 | return result; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /.config/jest/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | /* 8 | * This utility function is useful in combination with jest `transformIgnorePatterns` config 9 | * to transform specific packages (e.g.ES modules) in a projects node_modules folder. 10 | */ 11 | const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`; 12 | 13 | // Array of known nested grafana package dependencies that only bundle an ESM version 14 | const grafanaESModules = [ 15 | '.pnpm', // Support using pnpm symlinked packages 16 | '@grafana/schema', 17 | 'd3', 18 | 'd3-color', 19 | 'd3-force', 20 | 'd3-interpolate', 21 | 'd3-scale-chromatic', 22 | 'ol', 23 | 'react-colorful', 24 | 'rxjs', 25 | 'uuid', 26 | ]; 27 | 28 | module.exports = { 29 | nodeModulesToTransform, 30 | grafanaESModules, 31 | }; 32 | -------------------------------------------------------------------------------- /pkg/models/settings.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | ) 9 | 10 | type PluginSettings struct { 11 | Path string `json:"path"` 12 | InitSql string `json:"initSql"` 13 | Secrets *SecretPluginSettings `json:"-"` 14 | } 15 | 16 | type SecretPluginSettings struct { 17 | MotherDuckToken string `json:"motherduckToken"` 18 | } 19 | 20 | func LoadPluginSettings(source backend.DataSourceInstanceSettings) (*PluginSettings, error) { 21 | settings := PluginSettings{} 22 | err := json.Unmarshal(source.JSONData, &settings) 23 | if err != nil { 24 | return nil, fmt.Errorf("could not unmarshal PluginSettings json: %w", err) 25 | } 26 | settings.Secrets = loadSecretPluginSettings(source.DecryptedSecureJSONData) 27 | 28 | return &settings, nil 29 | } 30 | 31 | func loadSecretPluginSettings(source map[string]string) *SecretPluginSettings { 32 | return &SecretPluginSettings{ 33 | MotherDuckToken: source["motherDuckToken"], 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub Action automates the process of building Grafana plugins. 2 | # (For more information, see https://github.com/grafana/plugin-actions/blob/main/build-plugin/README.md) 3 | name: Release 4 | 5 | on: 6 | push: 7 | tags: 8 | - 'v*' # Run workflow on version tags, e.g. v1.0.0. 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | release: 14 | permissions: 15 | contents: write 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: grafana/plugin-actions/build-plugin@release 20 | # Uncomment to enable plugin signing 21 | # (For more info on how to generate the access policy token see https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin#generate-an-access-policy-token) 22 | with: 23 | # Make sure to save the token in your repository secrets 24 | policy_token: ${{ secrets.GRAFANA_PLUGIN_SIGNING_TOKEN }} 25 | # Usage of GRAFANA_API_KEY is deprecated, prefer `policy_token` option above 26 | #grafana_token: $ 27 | -------------------------------------------------------------------------------- /tests/queryVariables.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@grafana/plugin-e2e'; 2 | 3 | test('data source supports multiple query variables in one dashboard', async ({ 4 | readProvisionedDashboard, 5 | selectors, 6 | gotoDashboardPage 7 | }) => { 8 | const dashboard = await readProvisionedDashboard({fileName: 'variables.json' }); 9 | const dashboardPage = await gotoDashboardPage(dashboard); 10 | 11 | const query0Label = await dashboardPage.getByGrafanaSelector( 12 | selectors.pages.Dashboard.SubMenu.submenuItemLabels("query0") 13 | ); 14 | const query0DropdownFromLabel = await query0Label.locator('~ * [data-testid*="DropDown"]').first(); 15 | await expect(query0DropdownFromLabel).toHaveText('1'); 16 | 17 | 18 | const query1Label = await dashboardPage.getByGrafanaSelector( 19 | selectors.pages.Dashboard.SubMenu.submenuItemLabels("species") 20 | ); 21 | const query1DropdownFromLabel = await query1Label.locator('~ * [data-testid*="DropDown"]').first(); 22 | await expect(query1DropdownFromLabel).toHaveText('duck'); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /tests/queryEditor.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@grafana/plugin-e2e'; 2 | 3 | test('data query should return values 10 and 20', async ({ panelEditPage, readProvisionedDataSource, page }) => { 4 | const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' }); 5 | await panelEditPage.datasource.set(ds.name); 6 | await panelEditPage.getQueryEditorRow('A').getByRole("radiogroup").getByLabel("Code").click(); 7 | await panelEditPage.getQueryEditorRow('A').getByLabel("Editor content;Press Alt+F1 for Accessibility Options.").fill('select 10 as val union select 20 as val'); 8 | await panelEditPage.setVisualization('Table'); 9 | await panelEditPage.getQueryEditorRow('A').getByLabel("Query editor Run button").click(); 10 | 11 | // 12.2.0 or higher uses the new data grid 12 | const GrafanaVersion = (process.env.GRAFANA_VERSION || ''); 13 | if (GrafanaVersion >= '12.2.0') { 14 | const grid = page.locator('[role="grid"]'); 15 | await expect(grid).toContainText(['10']); 16 | await expect(grid).toContainText(['20']); 17 | } else { 18 | await expect(panelEditPage.panel.data).toContainText(['10']); 19 | await expect(panelEditPage.panel.data).toContainText(['20']); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /pkg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" 9 | "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" 10 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 11 | "github.com/motherduckdb/grafana-duckdb-datasource/pkg/plugin" 12 | ) 13 | 14 | func main() { 15 | // Start listening to requests sent from Grafana. This call is blocking so 16 | // it won't finish until Grafana shuts down the process or the plugin choose 17 | // to exit by itself using os.Exit. Manage automatically manages life cycle 18 | // of datasource instances. It accepts datasource instance factory as first 19 | // argument. This factory will be automatically called on incoming request 20 | // from Grafana to create different instances of SampleDatasource (per datasource 21 | // ID). When datasource configuration changed Dispose method will be called and 22 | // new datasource instance created using NewSampleDatasource factory. 23 | 24 | if err := datasource.Manage("motherduck-duckdb-datasource", datasourceFactory, datasource.ManageOpts{}); err != nil { 25 | log.DefaultLogger.Error(err.Error()) 26 | os.Exit(1) 27 | } 28 | } 29 | 30 | func datasourceFactory(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { 31 | ds := plugin.NewDatasource(&plugin.DuckDBDriver{Initialized: false}) 32 | return ds.NewDatasource(ctx, s) 33 | } 34 | -------------------------------------------------------------------------------- /tests/configEditor.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@grafana/plugin-e2e'; 2 | import { DuckDBDataSourceOptions, SecureJsonData } from '../src/types'; 3 | import { execSync } from 'child_process'; 4 | import path from 'path'; 5 | import fs from 'fs'; 6 | 7 | test('"Save & test" should be successful when configuration is valid', async ({ 8 | createDataSourceConfigPage, 9 | readProvisionedDataSource, 10 | page, 11 | }) => { 12 | const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' }); 13 | const configPage = await createDataSourceConfigPage({ type: ds.type }); 14 | await page.getByRole('textbox', { name: 'Database name' }).fill(""); 15 | await page.getByRole('textbox', { name: 'MotherDuck Token' }).fill(""); 16 | await expect(configPage.saveAndTest()).toBeOK(); 17 | }); 18 | 19 | test('"Save & test" should fail when configuration is invalid', async ({ 20 | createDataSourceConfigPage, 21 | readProvisionedDataSource, 22 | page, 23 | }) => { 24 | const ds = await readProvisionedDataSource({ fileName: 'datasources.yml' }); 25 | const configPage = await createDataSourceConfigPage({ type: ds.type }); 26 | await page.getByRole('textbox', { name: 'Database name' }).fill("md:"); 27 | await page.getByRole('textbox', { name: 'MotherDuck Token' }).fill(""); 28 | await expect(configPage.saveAndTest()).not.toBeOK(); 29 | await expect(configPage).toHaveAlert('error', { hasText: 'MotherDuck Token is missing for motherduck connection' }); 30 | }); 31 | -------------------------------------------------------------------------------- /cmd/preinstall-extensions/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/motherduckdb/grafana-duckdb-datasource/cmd/preinstall-extensions 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.1 6 | 7 | require github.com/marcboeker/go-duckdb/v2 v2.3.2 8 | 9 | require ( 10 | github.com/apache/arrow-go/v18 v18.1.0 // indirect 11 | github.com/duckdb/duckdb-go-bindings v0.1.16 // indirect 12 | github.com/duckdb/duckdb-go-bindings/darwin-amd64 v0.1.11 // indirect 13 | github.com/duckdb/duckdb-go-bindings/darwin-arm64 v0.1.11 // indirect 14 | github.com/duckdb/duckdb-go-bindings/linux-amd64 v0.1.11 // indirect 15 | github.com/duckdb/duckdb-go-bindings/linux-arm64 v0.1.11 // indirect 16 | github.com/duckdb/duckdb-go-bindings/windows-amd64 v0.1.11 // indirect 17 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 18 | github.com/goccy/go-json v0.10.5 // indirect 19 | github.com/google/flatbuffers v25.1.24+incompatible // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/klauspost/compress v1.17.11 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 23 | github.com/marcboeker/go-duckdb/arrowmapping v0.0.9 // indirect 24 | github.com/marcboeker/go-duckdb/mapping v0.0.10 // indirect 25 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 26 | github.com/zeebo/xxh3 v1.0.2 // indirect 27 | golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect 28 | golang.org/x/mod v0.22.0 // indirect 29 | golang.org/x/sync v0.10.0 // indirect 30 | golang.org/x/sys v0.29.0 // indirect 31 | golang.org/x/tools v0.29.0 // indirect 32 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /.config/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | const path = require('path'); 9 | const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils'); 10 | 11 | module.exports = { 12 | moduleNameMapper: { 13 | '\\.(css|scss|sass)$': 'identity-obj-proxy', 14 | 'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'), 15 | }, 16 | modulePaths: ['/src'], 17 | setupFilesAfterEnv: ['/jest-setup.js'], 18 | testEnvironment: 'jest-environment-jsdom', 19 | testMatch: [ 20 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 21 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 22 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 23 | ], 24 | transform: { 25 | '^.+\\.(t|j)sx?$': [ 26 | '@swc/jest', 27 | { 28 | sourceMaps: 'inline', 29 | jsc: { 30 | parser: { 31 | syntax: 'typescript', 32 | tsx: true, 33 | decorators: false, 34 | dynamicImport: true, 35 | }, 36 | }, 37 | }, 38 | ], 39 | }, 40 | // Jest will throw `Cannot use import statement outside module` if it tries to load an 41 | // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest 42 | transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)], 43 | }; 44 | -------------------------------------------------------------------------------- /.config/supervisord/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | 5 | [program:grafana] 6 | user=root 7 | directory=/var/lib/grafana 8 | command=bash -c 'while [ ! -f /root/motherduck-duckdb-datasource/dist/gpx_duckdb_datasource* ]; do sleep 1; done; /run.sh' 9 | stdout_logfile=/dev/fd/1 10 | stdout_logfile_maxbytes=0 11 | redirect_stderr=true 12 | killasgroup=true 13 | stopasgroup=true 14 | autostart=true 15 | 16 | [program:delve] 17 | user=root 18 | command=/bin/bash -c 'pid=""; while [ -z "$pid" ]; do pid=$(pgrep -f gpx_duckdb_datasource); done; /root/go/bin/dlv attach --api-version=2 --headless --continue --accept-multiclient --listen=:2345 $pid' 19 | stdout_logfile=/dev/fd/1 20 | stdout_logfile_maxbytes=0 21 | redirect_stderr=true 22 | killasgroup=false 23 | stopasgroup=false 24 | autostart=true 25 | autorestart=true 26 | 27 | [program:build-watcher] 28 | user=root 29 | command=/bin/bash -c 'while inotifywait -e modify,create,delete -r /var/lib/grafana/plugins/motherduck-duckdb-datasource; do echo "Change detected, restarting delve...";supervisorctl restart delve; done' 30 | stdout_logfile=/dev/fd/1 31 | stdout_logfile_maxbytes=0 32 | redirect_stderr=true 33 | killasgroup=true 34 | stopasgroup=true 35 | autostart=true 36 | 37 | [program:mage-watcher] 38 | user=root 39 | environment=PATH="/usr/local/go/bin:/root/go/bin:%(ENV_PATH)s" 40 | directory=/root/motherduck-duckdb-datasource 41 | command=/bin/bash -c 'git config --global --add safe.directory /root/motherduck-duckdb-datasource && mage -v watch' 42 | stdout_logfile=/dev/fd/1 43 | stdout_logfile_maxbytes=0 44 | redirect_stderr=true 45 | killasgroup=true 46 | stopasgroup=true 47 | autostart=true 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Grafana DuckDB Data Source Plugin - How To Contribute 2 | 3 | ## Local Development 4 | 5 | ### Prerequisites 6 | 7 | - Node.js (v20+) 8 | - Go (v1.21+), Mage, gcc (building the backend requires CGO) 9 | 10 | ### Building Locally 11 | 12 | #### Backend 13 | 14 | To build the backend plugin binary for your platform, run: 15 | 16 | ```bash 17 | mage -v build: build:GenerateManifestFile 18 | ``` 19 | possible values for `` are: `Linux`, `Windows`, `Darwin`, `DarwinARM64`, `LinuxARM64`, `LinuxARM`. 20 | 21 | Note: There's no clear way to cross-compile the plugin since it involves cross-compiling DuckDB via CGO. 22 | 23 | #### Frontend 24 | 25 | 1. Install dependencies 26 | 27 | ```bash 28 | npm install 29 | ``` 30 | 31 | 2. Build plugin in development mode and run in watch mode 32 | 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | 3. Build plugin in production mode 38 | 39 | ```bash 40 | npm run build 41 | ``` 42 | 43 | 4. Run the tests (using Jest) 44 | 45 | ```bash 46 | # Runs the tests and watches for changes, requires git init first 47 | npm run test 48 | 49 | # Exits after running all the tests 50 | npm run test:ci 51 | ``` 52 | 53 | 5. Spin up a Grafana instance and run the plugin inside it (using Docker) 54 | 55 | ```bash 56 | npm run server 57 | ``` 58 | 59 | 6. Run the E2E tests (using Cypress) 60 | 61 | ```bash 62 | # Spins up a Grafana instance first that we tests against 63 | npm run server 64 | 65 | # Starts the tests 66 | npm run e2e 67 | ``` 68 | 69 | 7. Run the linter 70 | 71 | ```bash 72 | npm run lint 73 | 74 | # or 75 | 76 | npm run lint:fix 77 | ``` -------------------------------------------------------------------------------- /cmd/preinstall-extensions/main.go: -------------------------------------------------------------------------------- 1 | // From https://github.com/snakedotdev/grafana-duckdb-datasource 2 | // Apache 2.0 Licensed 3 | // Copyright snakedotdev 4 | // Modified from original version 5 | 6 | package main 7 | 8 | import ( 9 | "database/sql" 10 | "flag" 11 | "fmt" 12 | "log" 13 | "path/filepath" 14 | "strings" 15 | 16 | _ "github.com/marcboeker/go-duckdb/v2" 17 | ) 18 | 19 | func main() { 20 | var ( 21 | sqlCmd = flag.String("s", "", "SQL command to execute (multiple commands can be separated by semicolons)") 22 | dbPath = flag.String("db", "", "Path to DuckDB database file (empty for in-memory)") 23 | ) 24 | 25 | flag.Parse() 26 | 27 | if *sqlCmd == "" { 28 | log.Fatal("SQL command is required") 29 | } 30 | 31 | // Prepare database path 32 | dsn := "?access_mode=read_write" 33 | if *dbPath != "" { 34 | absPath, err := filepath.Abs(*dbPath) 35 | if err != nil { 36 | log.Fatalf("Failed to get absolute path: %v", err) 37 | } 38 | dsn = absPath + dsn 39 | } 40 | 41 | fmt.Printf("Using DSN: %s\n", dsn) 42 | 43 | // Open database connection 44 | db, err := sql.Open("duckdb", dsn) 45 | if err != nil { 46 | log.Fatalf("Failed to open database: %v", err) 47 | } 48 | defer db.Close() 49 | 50 | // Split the input command into individual commands 51 | commands := strings.Split(*sqlCmd, ";") 52 | 53 | // Execute each command 54 | for _, cmd := range commands { 55 | // Skip empty commands 56 | cmd = strings.TrimSpace(cmd) 57 | if cmd == "" { 58 | continue 59 | } 60 | 61 | fmt.Printf("Executing: %s\n", cmd) 62 | result, err := db.Exec(cmd) 63 | if err != nil { 64 | log.Fatalf("Failed to execute SQL '%s': %v", cmd, err) 65 | } 66 | if result != nil { 67 | rows, _ := result.RowsAffected() 68 | fmt.Printf("Rows affected: %d\n", rows) 69 | } 70 | } 71 | 72 | fmt.Println("All commands executed successfully") 73 | } 74 | -------------------------------------------------------------------------------- /.config/webpack/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import process from 'process'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import { glob } from 'glob'; 6 | import { SOURCE_DIR } from './constants'; 7 | 8 | export function isWSL() { 9 | if (process.platform !== 'linux') { 10 | return false; 11 | } 12 | 13 | if (os.release().toLowerCase().includes('microsoft')) { 14 | return true; 15 | } 16 | 17 | try { 18 | return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); 19 | } catch { 20 | return false; 21 | } 22 | } 23 | 24 | export function getPackageJson() { 25 | return require(path.resolve(process.cwd(), 'package.json')); 26 | } 27 | 28 | export function getPluginJson() { 29 | return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); 30 | } 31 | 32 | export function hasReadme() { 33 | return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); 34 | } 35 | 36 | // Support bundling nested plugins by finding all plugin.json files in src directory 37 | // then checking for a sibling module.[jt]sx? file. 38 | export async function getEntries(): Promise> { 39 | const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); 40 | 41 | const plugins = await Promise.all( 42 | pluginsJson.map((pluginJson) => { 43 | const folder = path.dirname(pluginJson); 44 | return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); 45 | }) 46 | ); 47 | 48 | return plugins.reduce((result, modules) => { 49 | return modules.reduce((result, module) => { 50 | const pluginPath = path.dirname(module); 51 | const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); 52 | const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; 53 | 54 | result[entryName] = module; 55 | return result; 56 | }, result); 57 | }, {}); 58 | } 59 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOptions } from '@grafana/plugin-e2e'; 2 | import { defineConfig, devices } from '@playwright/test'; 3 | import { dirname } from 'node:path'; 4 | 5 | const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`; 6 | 7 | /** 8 | * Read environment variables from file. 9 | * https://github.com/motdotla/dotenv 10 | */ 11 | // require('dotenv').config(); 12 | 13 | /** 14 | * See https://playwright.dev/docs/test-configuration. 15 | */ 16 | export default defineConfig({ 17 | testDir: './tests', 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: 'html', 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Base URL to use in actions like `await page.goto('/')`. */ 31 | baseURL: 'http://localhost:3000', 32 | 33 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 34 | trace: 'on-first-retry', 35 | }, 36 | 37 | /* Configure projects for major browsers */ 38 | projects: [ 39 | // 1. Login to Grafana and store the cookie on disk for use in other tests. 40 | { 41 | name: 'auth', 42 | testDir: pluginE2eAuth, 43 | testMatch: [/.*\.js/], 44 | }, 45 | // 2. Run tests in Google Chrome. Every test will start authenticated as admin user. 46 | { 47 | name: 'chromium', 48 | use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json' }, 49 | dependencies: ['auth'], 50 | }, 51 | ], 52 | }); 53 | -------------------------------------------------------------------------------- /src/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /provisioning/dashboards/variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "links": [], 22 | "panels": [], 23 | "preload": false, 24 | "schemaVersion": 41, 25 | "tags": [], 26 | "templating": { 27 | "list": [ 28 | { 29 | "current": { 30 | "text": "1", 31 | "value": "1" 32 | }, 33 | "datasource": { 34 | "type": "motherduck-duckdb-datasource", 35 | "uid": "42" 36 | }, 37 | "definition": "select 1", 38 | "label": "query0", 39 | "name": "query0", 40 | "options": [], 41 | "query": "select 1", 42 | "refresh": 1, 43 | "regex": "", 44 | "type": "query" 45 | }, 46 | { 47 | "current": { 48 | "text": "duck", 49 | "value": "duck" 50 | }, 51 | "datasource": { 52 | "type": "motherduck-duckdb-datasource", 53 | "uid": "42" 54 | }, 55 | "definition": "select 'duck' as species", 56 | "name": "species", 57 | "options": [], 58 | "query": "select 'duck' as species", 59 | "refresh": 1, 60 | "regex": "", 61 | "type": "query" 62 | } 63 | ] 64 | }, 65 | "time": { 66 | "from": "now-6h", 67 | "to": "now" 68 | }, 69 | "timepicker": {}, 70 | "timezone": "browser", 71 | "title": "VariablesTestDashboard", 72 | "uid": "99999", 73 | "version": 5 74 | } -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Duckdb-Datasource 6 | 7 | Duck db and mother duck datasource for grafana 8 | 9 | 52 | -------------------------------------------------------------------------------- /.config/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG grafana_version=latest 2 | ARG grafana_image=grafana-enterprise 3 | 4 | FROM grafana/${grafana_image}:${grafana_version}-ubuntu 5 | 6 | ARG development=true 7 | ARG TARGETARCH 8 | 9 | ARG GO_VERSION=1.21.6 10 | ARG GO_ARCH=${TARGETARCH:-amd64} 11 | 12 | ENV DEV "${development}" 13 | 14 | # Make it as simple as possible to access the grafana instance for development purposes 15 | # Do NOT enable these settings in a public facing / production grafana instance 16 | ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" 17 | ENV GF_AUTH_ANONYMOUS_ENABLED "true" 18 | ENV GF_AUTH_BASIC_ENABLED "false" 19 | # Set development mode so plugins can be loaded without the need to sign 20 | ENV GF_DEFAULT_APP_MODE "development" 21 | 22 | 23 | LABEL maintainer="Grafana Labs " 24 | 25 | ENV GF_PATHS_HOME="/usr/share/grafana" 26 | WORKDIR $GF_PATHS_HOME 27 | 28 | USER root 29 | 30 | # Installing supervisor and inotify-tools and gdb 31 | RUN if [ "${development}" = "true" ]; then \ 32 | if grep -i -q alpine /etc/issue; then \ 33 | apk add supervisor inotify-tools git; \ 34 | elif grep -i -q ubuntu /etc/issue; then \ 35 | DEBIAN_FRONTEND=noninteractive && \ 36 | apt-get update && \ 37 | apt-get install -y supervisor inotify-tools git g++ && \ 38 | rm -rf /var/lib/apt/lists/*; \ 39 | else \ 40 | echo 'ERROR: Unsupported base image' && /bin/false; \ 41 | fi \ 42 | fi 43 | 44 | COPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini 45 | COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf 46 | 47 | 48 | # Installing Go 49 | RUN if [ "${development}" = "true" ]; then \ 50 | curl -O -L https://golang.org/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ 51 | rm -rf /usr/local/go && \ 52 | tar -C /usr/local -xzf go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ 53 | echo "export PATH=$PATH:/usr/local/go/bin:~/go/bin" >> ~/.bashrc && \ 54 | rm -f go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \ 55 | fi 56 | 57 | # Installing delve for debugging 58 | RUN if [ "${development}" = "true" ]; then \ 59 | /usr/local/go/bin/go install github.com/go-delve/delve/cmd/dlv@latest; \ 60 | fi 61 | 62 | # Installing mage for plugin (re)building 63 | RUN if [ "${development}" = "true" ]; then \ 64 | git clone https://github.com/magefile/mage; \ 65 | cd mage; \ 66 | export PATH=$PATH:/usr/local/go/bin; \ 67 | go run bootstrap.go; \ 68 | fi 69 | 70 | # Inject livereload script into grafana index.html 71 | RUN sed -i 's|||g' /usr/share/grafana/public/views/index.html 72 | 73 | 74 | COPY entrypoint.sh /entrypoint.sh 75 | RUN chmod +x /entrypoint.sh 76 | ENTRYPOINT ["/entrypoint.sh"] 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duckdb-datasource", 3 | "version": "0.4.0", 4 | "description": "DuckDB and MotherDuck datasource for grafana", 5 | "scripts": { 6 | "build": "webpack -c ./.config/webpack/webpack.config.ts --env production", 7 | "dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development", 8 | "test": "jest --watch --onlyChanged", 9 | "test:ci": "jest --passWithNoTests --maxWorkers 4", 10 | "typecheck": "tsc --noEmit", 11 | "lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .", 12 | "lint:fix": "npm run lint -- --fix", 13 | "e2e": "playwright test --trace on", 14 | "server": "docker-compose up --build", 15 | "sign": "npx --yes @grafana/sign-plugin@latest", 16 | "postinstall": "patch-package" 17 | }, 18 | "author": "Motherduck", 19 | "license": "Apache-2.0", 20 | "devDependencies": { 21 | "@babel/core": "^7.21.4", 22 | "@grafana/eslint-config": "latest", 23 | "@grafana/plugin-e2e": "latest", 24 | "@grafana/tsconfig": "2.0.0", 25 | "@playwright/test": "^1.41.2", 26 | "@swc/core": "^1.3.90", 27 | "@swc/helpers": "^0.5.0", 28 | "@swc/jest": "^0.2.26", 29 | "@testing-library/dom": "^10.4.0", 30 | "@testing-library/jest-dom": "^6.4.6", 31 | "@types/jest": "^29.5.0", 32 | "@types/lodash": "^4.14.194", 33 | "@types/node": "^20.8.7", 34 | "@types/react-router-dom": "^5.2.0", 35 | "@types/testing-library__jest-dom": "5.14.8", 36 | "copy-webpack-plugin": "^11.0.0", 37 | "css-loader": "^6.7.3", 38 | "eslint-plugin-deprecation": "^2.0.0", 39 | "eslint-webpack-plugin": "^4.0.1", 40 | "fork-ts-checker-webpack-plugin": "^8.0.0", 41 | "glob": "^10.2.7", 42 | "identity-obj-proxy": "3.0.0", 43 | "jest": "^29.5.0", 44 | "jest-environment-jsdom": "^29.5.0", 45 | "prettier": "^2.8.7", 46 | "replace-in-file-webpack-plugin": "^1.0.6", 47 | "sass": "1.63.2", 48 | "sass-loader": "13.3.1", 49 | "sql-formatter-plus": "^1.3.6", 50 | "style-loader": "3.3.3", 51 | "swc-loader": "^0.2.3", 52 | "ts-node": "^10.9.1", 53 | "tsconfig-paths": "^4.2.0", 54 | "typescript": "^5.2.0", 55 | "webpack": "^5.86.0", 56 | "webpack-cli": "^5.1.4", 57 | "webpack-livereload-plugin": "^3.0.2" 58 | }, 59 | "engines": { 60 | "node": ">=20" 61 | }, 62 | "dependencies": { 63 | "@grafana/data": "latest", 64 | "@grafana/experimental": "latest", 65 | "@grafana/plugin-ui": "^0.9.4", 66 | "@grafana/runtime": "latest", 67 | "@grafana/ui": "latest", 68 | "patch-package": "^8.0.0", 69 | "react": "^18.0.0", 70 | "react-dom": "^18.0.0", 71 | "tslib": "2.5.3" 72 | }, 73 | "overrides": { 74 | "@grafana/data": "latest", 75 | "@grafana/experimental": "latest", 76 | "@grafana/runtime": "latest", 77 | "@grafana/ui": "latest", 78 | "@emotion/css": "11.11.2" 79 | }, 80 | "resolutions": { 81 | "react": "17.0.2", 82 | "react-dom": "17.0.2" 83 | }, 84 | "packageManager": "npm@10.5.0" 85 | } 86 | -------------------------------------------------------------------------------- /src/components/ConfigEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react'; 2 | import { InlineField, Input, SecretInput, TextArea } from '@grafana/ui'; 3 | import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; 4 | import { DuckDBDataSourceOptions, SecureJsonData } from '../types'; 5 | 6 | interface Props extends DataSourcePluginOptionsEditorProps {} 7 | 8 | export function ConfigEditor(props: Props) { 9 | const { onOptionsChange, options } = props; 10 | const { jsonData, secureJsonFields, secureJsonData } = options; 11 | 12 | const onPathChange = (event: ChangeEvent) => { 13 | onOptionsChange({ 14 | ...options, 15 | jsonData: { 16 | ...jsonData, 17 | path: event.target.value, 18 | }, 19 | }); 20 | }; 21 | 22 | const onInitSqlChange = (event: ChangeEvent) => { 23 | onOptionsChange({ 24 | ...options, 25 | jsonData: { 26 | ...jsonData, 27 | initSql: event.target.value, 28 | }, 29 | }); 30 | }; 31 | 32 | 33 | // Secure field (only sent to the backend) 34 | const onMotherDuckTokenChange = (event: ChangeEvent) => { 35 | onOptionsChange({ 36 | ...options, 37 | secureJsonData: { 38 | motherDuckToken: event.target.value, 39 | }, 40 | }); 41 | }; 42 | 43 | const onResetMotherDuckToken = () => { 44 | onOptionsChange({ 45 | ...options, 46 | secureJsonFields: { 47 | motherDuckToken: false, 48 | }, 49 | secureJsonData: { 50 | motherDuckToken: '', 51 | }, 52 | }); 53 | }; 54 | 55 | return ( 56 | <> 57 | 58 | 65 | 66 | 68 |