├── .gitattributes ├── .github ├── FUNDING.yml ├── workflows │ ├── stale.yml │ ├── branches.yml │ └── tags.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .tool-versions ├── .config ├── .cprc.json ├── webpack │ ├── constants.ts │ ├── BuildModeWebpackPlugin.ts │ ├── utils.ts │ └── webpack.config.ts ├── .prettierrc.js ├── entrypoint.sh ├── types │ ├── bundler-rules.d.ts │ ├── custom.d.ts │ └── webpack-plugins.d.ts ├── tsconfig.json ├── .eslintrc ├── jest │ ├── mocks │ │ └── react-inlinesvg.tsx │ └── utils.js ├── jest-setup.js ├── docker-compose-base.yaml ├── jest.config.js ├── supervisord │ └── supervisord.conf ├── Dockerfile └── README.md ├── .eslintrc ├── tsconfig.json ├── grafana_config ├── data.db ├── dashboard.yaml ├── grafana.ini ├── datasource.yaml └── dashboards │ ├── graph_and_variables.json │ ├── query_and_repitition.json │ └── alert.json ├── src ├── img │ ├── sine-wave-example-dashboard.png │ ├── sine-wave-timeseries-panel-edit.png │ └── logo.svg ├── module.ts ├── types.ts ├── plugin.json ├── DataSource.ts ├── test │ └── template_srv.ts ├── QueryEditor.test.tsx ├── DataSource.test.ts ├── QueryEditor.tsx └── ConfigEditor.tsx ├── .prettierrc.js ├── .cprc.json ├── tools.go ├── SECURITY.md ├── githooks └── pre-push ├── .gitignore ├── jest-setup.js ├── jest.config.js ├── pkg ├── plugin │ ├── variables.go │ ├── json_support_test.go │ ├── macros.go │ ├── check_health.go │ ├── sqlite_datasource.go │ ├── query_time_test.go │ ├── gap_filling.go │ ├── query_timeseries_test.go │ ├── query_types_test.go │ ├── check_health_test.go │ └── query_test.go └── main.go ├── .releaserc.json ├── selenium ├── graph_and_variables.test.ts ├── configure.test.ts ├── helpers.ts ├── alerting.test.ts ├── query_and_repetition.test.ts └── writing_queries.test.ts ├── Magefile.go ├── release_template.md ├── docker-compose.yaml ├── docs ├── installation.md ├── examples.md └── faq.md ├── DEVELOPMENT.md ├── Makefile ├── package.json ├── go.mod ├── README.md └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.db binary 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [fr-ser] 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 22.11.0 2 | golang 1.24.4 3 | -------------------------------------------------------------------------------- /.config/.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.25.1" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/.eslintrc" 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.config/webpack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_DIR = 'src'; 2 | export const DIST_DIR = 'dist'; 3 | -------------------------------------------------------------------------------- /grafana_config/data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr-ser/grafana-sqlite-datasource/HEAD/grafana_config/data.db -------------------------------------------------------------------------------- /src/img/sine-wave-example-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr-ser/grafana-sqlite-datasource/HEAD/src/img/sine-wave-example-dashboard.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Prettier configuration provided by Grafana scaffolding 3 | ...require("./.config/.prettierrc.js") 4 | }; 5 | -------------------------------------------------------------------------------- /.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "bundleGrafanaUI": false, 4 | "useReactRouterV6": true, 5 | "useExperimentalRspack": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/img/sine-wave-timeseries-panel-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fr-ser/grafana-sqlite-datasource/HEAD/src/img/sine-wave-timeseries-panel-edit.png -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package plugin 5 | 6 | import ( 7 | _ "github.com/magefile/mage" 8 | _ "gotest.tools/gotestsum" 9 | ) 10 | -------------------------------------------------------------------------------- /grafana_config/dashboard.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: "Test Dashboards" 5 | orgId: 1 6 | folder: "" 7 | type: file 8 | disableDeletion: true 9 | editable: true 10 | updateIntervalSeconds: 300 11 | allowUiUpdates: true 12 | options: 13 | path: /app/dashboards 14 | -------------------------------------------------------------------------------- /grafana_config/grafana.ini: -------------------------------------------------------------------------------- 1 | # possible values : production, development 2 | app_mode = development 3 | 4 | [security] 5 | admin_user = admin 6 | admin_password = admin123 7 | [log] 8 | level = debug 9 | [plugins] 10 | allow_loading_unsigned_plugins = true 11 | 12 | [plugin.frser-sqlite-datasource] 13 | block_list = secret.db,secret2 14 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { DataSource } from './DataSource'; 3 | import { ConfigEditor } from './ConfigEditor'; 4 | import { QueryEditor } from './QueryEditor'; 5 | import { SQLiteQuery, MyDataSourceOptions } from './types'; 6 | 7 | export const plugin = new DataSourcePlugin(DataSource) 8 | .setConfigEditor(ConfigEditor) 9 | .setQueryEditor(QueryEditor); 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest version is supported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | You can use the Github feature for privately reporting a security vulnerability. 10 | 11 | For instructions on how to do so, you can follow this link: 12 | 13 | -------------------------------------------------------------------------------- /githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$(uname)" == "Darwin" ]; then 4 | extended_sed="sed -E" 5 | else 6 | extended_sed="sed -r" 7 | fi 8 | 9 | latest_tag=$(git describe --tags --abbrev=0) 10 | 11 | latest_tag_without_rc=$(echo $latest_tag | ${extended_sed} s/-rc\.[0-9]+//) 12 | 13 | if ! grep -q -F "## [${latest_tag_without_rc:1}]" CHANGELOG.md; then 14 | echo "Error: The (## [${latest_tag_without_rc:1}]) tag was not found in the CHANGELOG.md." 15 | echo "Latest tag: ${latest_tag}" 1>&2 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | node_modules/ 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Compiled binary addons (https://nodejs.org/api/addons.html) 21 | dist/ 22 | artifacts/ 23 | work/ 24 | ci/ 25 | e2e-results/ 26 | **/cypress/videos 27 | **/cypress/report.json 28 | 29 | # Editor 30 | .idea 31 | .vscode 32 | 33 | .eslintcache 34 | -------------------------------------------------------------------------------- /grafana_config/datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: sqlite 4 | type: frser-sqlite-datasource 5 | access: proxy 6 | isDefault: true 7 | editable: true 8 | jsonData: 9 | path: /app/data.db 10 | - name: postgres 11 | type: postgres 12 | access: proxy 13 | url: postgres:5432 14 | user: admin 15 | database: db_name 16 | isDefault: false 17 | editable: true 18 | jsonData: 19 | postgresVersion: 1200 20 | timescaledb: false 21 | maxOpenConns: 5 22 | sslmode: "disable" 23 | secureJsonData: 24 | password: changed_later 25 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | // Jest setup provided by Grafana scaffolding 2 | import './.config/jest-setup'; 3 | 4 | // mock the intersection observer and just say everything is in view 5 | // copied from https://github.com/grafana/grafana/blob/0a90b7b5e9c6fc691743389b78f8116427ec5210/public/test/jest-setup.ts#L43C1-L53C56 6 | const mockIntersectionObserver = jest.fn().mockImplementation((callback) => ({ 7 | observe: jest.fn().mockImplementation((elem) => { 8 | callback([{ target: elem, isIntersecting: true }]); 9 | }), 10 | unobserve: jest.fn(), 11 | disconnect: jest.fn(), 12 | })); 13 | global.IntersectionObserver = mockIntersectionObserver; 14 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | 3 | on: 4 | schedule: 5 | - cron: '30 1 * * *' 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | days-before-pr-close: -1 16 | days-before-issue-stale: 30 17 | days-before-issue-close: 14 18 | stale-issue-label: pending-closure 19 | exempt-issue-labels: TODO 20 | stale-issue-message: > 21 | This issue has been automatically marked as stale because it has not had recent activity. 22 | It will be closed if no further activity occurs. 23 | Thank you for your contributions. 24 | -------------------------------------------------------------------------------- /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 | const { grafanaESModules, nodeModulesToTransform } = require('./.config/jest/utils'); 5 | 6 | module.exports = { 7 | // Jest configuration provided by @grafana/create-plugin 8 | ...require('./.config/jest.config'), 9 | // Inform Jest to only transform specific node_module packages. 10 | transformIgnorePatterns: [ 11 | nodeModulesToTransform([ 12 | ...grafanaESModules, 13 | 'marked', 14 | 'react-calendar', 15 | 'get-user-locale', 16 | 'memoize', 17 | 'mimic-function', 18 | '@wojtekmaj/date-utils', 19 | ]), 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /pkg/plugin/variables.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | ) 9 | 10 | // replaceVariables replaces Grafana Template Variables in the query 11 | // this is mainly used for alert queries, which need time replacement 12 | func replaceVariables(queryConfig *queryConfigStruct, dataQuery backend.DataQuery) error { 13 | queryConfig.FinalQuery = strings.ReplaceAll( 14 | queryConfig.FinalQuery, 15 | "$__from", 16 | strconv.FormatInt(dataQuery.TimeRange.From.Unix()*1000, 10), 17 | ) 18 | queryConfig.FinalQuery = strings.ReplaceAll( 19 | queryConfig.FinalQuery, 20 | "$__to", 21 | strconv.FormatInt(dataQuery.TimeRange.To.Unix()*1000, 10), 22 | ) 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /.config/types/bundler-rules.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 src: string; 29 | export default src; 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/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/get-started/set-up-development-environment#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 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | ["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }], 5 | [ 6 | "@semantic-release/exec", 7 | { 8 | "verifyReleaseCmd": "grep -q -F '## [Unreleased]' CHANGELOG.md || (echo \"There was no header for '## [Unreleased]' in the CHANGELOG.md\" && exit 1)", 9 | "prepareCmd": "sed -i 's/## \\[Unreleased\\]/## [${nextRelease.version}] - ${new Date().toISOString().slice(0,10)}/g' CHANGELOG.md" 10 | } 11 | ], 12 | ["@semantic-release/npm", { "npmPublish": false }], 13 | [ 14 | "@semantic-release/git", 15 | { 16 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}", 17 | "assets": ["CHANGELOG.md", "package.json", "package-lock.json"] 18 | } 19 | ] 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.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/get-started/set-up-development-environment#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 | "files": ["src/**/*.{ts,tsx}"], 16 | "rules": { 17 | "@typescript-eslint/no-deprecated": "warn" 18 | }, 19 | "parserOptions": { 20 | "project": "./tsconfig.json" 21 | } 22 | }, 23 | { 24 | "files": ["./tests/**/*"], 25 | "rules": { 26 | "react-hooks/rules-of-hooks": "off" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Versions (please complete the following information):** 25 | 26 | - OS: [e.g. Ubuntu] 27 | - Run via docker? [e.g. yes] 28 | - Browser [e.g. chrome, safari] 29 | - Grafana Version [e.g. 7.3.3] 30 | - Plugin Version [e.g. 0.2.1] 31 | - Plugin installed via grafana-cli or manually? [e.g. grafana-cli] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. Please also attach the logs of Grafana! 35 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/get-started/set-up-development-environment#extend-the-jest-config 6 | */ 7 | 8 | import '@testing-library/jest-dom'; 9 | import { TextEncoder, TextDecoder } from 'util'; 10 | 11 | Object.assign(global, { TextDecoder, TextEncoder }); 12 | 13 | // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom 14 | Object.defineProperty(global, 'matchMedia', { 15 | writable: true, 16 | value: (query) => ({ 17 | matches: false, 18 | media: query, 19 | onchange: null, 20 | addListener: jest.fn(), // deprecated 21 | removeListener: jest.fn(), // deprecated 22 | addEventListener: jest.fn(), 23 | removeEventListener: jest.fn(), 24 | dispatchEvent: jest.fn(), 25 | }), 26 | }); 27 | 28 | HTMLCanvasElement.prototype.getContext = () => {}; 29 | -------------------------------------------------------------------------------- /.config/docker-compose-base.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | grafana: 3 | user: root 4 | container_name: 'frser-sqlite-datasource' 5 | 6 | build: 7 | context: . 8 | args: 9 | grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} 10 | grafana_version: ${GRAFANA_VERSION:-12.0.2} 11 | development: ${DEVELOPMENT:-false} 12 | anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true} 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/frser-sqlite-datasource 23 | - ../provisioning:/etc/grafana/provisioning 24 | - ..:/root/frser-sqlite-datasource 25 | 26 | environment: 27 | NODE_ENV: development 28 | GF_LOG_FILTERS: plugin.frser-sqlite-datasource:debug 29 | GF_LOG_LEVEL: debug 30 | GF_DATAPROXY_LOGGING: 1 31 | GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: frser-sqlite-datasource 32 | -------------------------------------------------------------------------------- /pkg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" 7 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 8 | 9 | "grafana-sqlite-datasource/pkg/plugin" 10 | ) 11 | 12 | func main() { 13 | // Start listening to requests sent from Grafana. This call is blocking so 14 | // it won't finish until Grafana shuts down the process or the plugin choose 15 | // to exit by itself using os.Exit. Manage automatically manages life cycle 16 | // of datasource instances. It accepts datasource instance factory as first 17 | // argument. This factory will be automatically called on incoming request 18 | // from Grafana to create different instances of SampleDatasource (per datasource 19 | // ID). When datasource configuration changed Dispose method will be called and 20 | // new datasource instance created using NewSampleDatasource factory. 21 | err := datasource.Manage("frser-sqlite-datasource", plugin.NewDataSource, datasource.ManageOpts{}) 22 | if err != nil { 23 | log.DefaultLogger.Error(err.Error()) 24 | os.Exit(1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { DataQuery, DataSourceJsonData } from '@grafana/data'; 2 | 3 | export interface SQLiteQuery extends DataQuery { 4 | rawQueryText: string; 5 | queryText: string; 6 | timeColumns: string[]; 7 | } 8 | 9 | export const defaultQuery: Partial = { 10 | rawQueryText: 11 | "SELECT CAST(strftime('%s', 'now', '-1 minute') as INTEGER) as time, 4 as value \n" + 12 | 'WHERE time >= $__from / 1000 and time < $__to / 1000', 13 | queryText: ` 14 | SELECT CAST(strftime('%s', 'now', '-1 minute') as INTEGER) as time, 4 as value 15 | WHERE time >= 1234 and time < 134567 16 | `, 17 | timeColumns: ['time', 'ts'], 18 | queryType: 'table', 19 | }; 20 | 21 | /** 22 | * These are options configured for each DataSource instance. 23 | * The values are optional because by default Grafana provides an empty 24 | * object (e.g. when adding a new data source) 25 | */ 26 | export interface MyDataSourceOptions extends DataSourceJsonData { 27 | path?: string; 28 | pathPrefix?: string; 29 | pathOptions?: string; 30 | attachLimit?: number; 31 | } 32 | export interface MySecureJsonData { 33 | securePathOptions?: string; 34 | } 35 | -------------------------------------------------------------------------------- /selenium/graph_and_variables.test.ts: -------------------------------------------------------------------------------- 1 | const { By, until } = require('selenium-webdriver'); 2 | 3 | import { getDriver, login, logHTMLOnFailure, saveTestState, GRAFANA_URL } from './helpers'; 4 | 5 | describe('graph and variables', () => { 6 | jest.setTimeout(30000); 7 | let driver; 8 | let testStatus = { ok: true }; 9 | 10 | beforeAll(async () => { 11 | driver = await getDriver(); 12 | 13 | await login(driver); 14 | await driver.get(`${GRAFANA_URL}/d/U6rjzWDMz/sine-wave-example`); 15 | await driver.wait(until.elementLocated(By.xpath(`//a[text()[contains(., "Sine Wave Example")]]`)), 5 * 1000); 16 | }); 17 | 18 | afterEach(async () => { 19 | await logHTMLOnFailure(testStatus, driver); 20 | testStatus.ok = true; 21 | }); 22 | 23 | afterAll(async () => { 24 | await driver.quit(); 25 | }); 26 | 27 | it( 28 | 'shows the aggregate sine wave values', 29 | saveTestState(testStatus, async () => { 30 | await driver.wait( 31 | until.elementLocated( 32 | By.xpath(`//div[contains(@aria-label, 'Sine Wave With Variable')]//a[text()[contains(., "avg(value)")]]`) 33 | ), 34 | 5 * 1000 35 | ); 36 | }) 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /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 | "github.com/magefile/mage/sh" 10 | ) 11 | 12 | // BuildAllAndMore uses the official BuildAll and runs some more steps 13 | func BuildAllAndMore() error { 14 | build.BuildAll() 15 | 16 | // freebsd is not included by default so we add it here 17 | err := sh.RunWith( 18 | map[string]string{"GOOS": "freebsd", "GOARCH": "amd64"}, 19 | "go", "build", "-o", "dist/gpx_sqlite-datasource_freebsd_amd64", "./pkg", 20 | ) 21 | 22 | if err != nil { 23 | return err 24 | } 25 | 26 | // Most 32-bit Raspberry Pi models have an ARMv6 architecture. (Pi Zero and 1 models) 27 | // All other models (2 Mod. B v1.2, 3 and 4) have an 64Bit ARMv8 architecture. 28 | // Only the Raspberry Pi 2 Mod. B has an ARMv7 architecture 29 | // To enable seamless installation for the raspberry pi zero we build for ARMv6 here. 30 | err = sh.RunWith( 31 | map[string]string{"GOOS": "linux", "GOARCH": "arm", "GOARM": "6"}, 32 | "go", "build", "-o", "dist/gpx_sqlite-datasource_linux_arm", "./pkg", 33 | ) 34 | 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /.config/webpack/BuildModeWebpackPlugin.ts: -------------------------------------------------------------------------------- 1 | import webpack, { type Compiler } from 'webpack'; 2 | 3 | const PLUGIN_NAME = 'BuildModeWebpack'; 4 | 5 | export class BuildModeWebpackPlugin { 6 | apply(compiler: webpack.Compiler) { 7 | compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { 8 | compilation.hooks.processAssets.tap( 9 | { 10 | name: PLUGIN_NAME, 11 | stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, 12 | }, 13 | async () => { 14 | const assets = compilation.getAssets(); 15 | for (const asset of assets) { 16 | if (asset.name.endsWith('plugin.json')) { 17 | const pluginJsonString = asset.source.source().toString(); 18 | const pluginJsonWithBuildMode = JSON.stringify( 19 | { 20 | ...JSON.parse(pluginJsonString), 21 | buildMode: compilation.options.mode, 22 | }, 23 | null, 24 | 4 25 | ); 26 | compilation.updateAsset(asset.name, new webpack.sources.RawSource(pluginJsonWithBuildMode)); 27 | } 28 | } 29 | } 30 | ); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/plugin/json_support_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | ) 11 | 12 | // TestJsonSupport tests against a query with a json function to make sure, that the plugin is 13 | // built with the JSON extension 14 | func TestJsonSupport(t *testing.T) { 15 | dbPath, cleanup := createTmpDB("SELECT 1 -- create db") 16 | defer cleanup() 17 | 18 | baseQuery := `SELECT json_array_length('[1,2,3,4]') as value;` 19 | dataQuery := getDataQuery(queryModel{QueryText: baseQuery}) 20 | 21 | response := query(dataQuery, pluginConfig{Path: dbPath}, context.Background()) 22 | if response.Error != nil { 23 | t.Errorf("Unexpected error - %s", response.Error) 24 | } 25 | 26 | if len(response.Frames) != 1 { 27 | t.Errorf( 28 | "Expected one frame but got - %d: Frames %+v", len(response.Frames), response.Frames, 29 | ) 30 | } 31 | 32 | expectedFrame := data.NewFrame( 33 | "", 34 | data.NewField("value", nil, []*int64{intPointer(4)}), 35 | ) 36 | expectedFrame.Meta = &data.FrameMeta{ExecutedQueryString: baseQuery} 37 | 38 | if diff := cmp.Diff(expectedFrame, response.Frames[0], cmpOption...); diff != "" { 39 | t.Error(diff) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "datasource", 3 | "name": "SQLite", 4 | "id": "frser-sqlite-datasource", 5 | "metrics": true, 6 | "backend": true, 7 | "alerting": true, 8 | "category": "sql", 9 | "executable": "gpx_sqlite-datasource", 10 | "annotations": true, 11 | "info": { 12 | "description": "SQLite as a (Backend) Datasource", 13 | "author": { 14 | "name": "Sergej Herbert", 15 | "url": "https://github.com/fr-ser" 16 | }, 17 | "keywords": ["sqlite", "sql", "raspberry", "arm"], 18 | "logos": { 19 | "small": "img/logo.svg", 20 | "large": "img/logo.svg" 21 | }, 22 | "links": [ 23 | { 24 | "name": "Website", 25 | "url": "https://github.com/fr-ser/grafana-sqlite-datasource" 26 | }, 27 | { 28 | "name": "License", 29 | "url": "https://github.com/fr-ser/grafana-sqlite-datasource/blob/main/LICENSE" 30 | }, 31 | { 32 | "name": "Sponsor", 33 | "url": "https://github.com/sponsors/fr-ser" 34 | } 35 | ], 36 | "screenshots": [ 37 | { 38 | "name": "Example Dashboard", 39 | "path": "img/sine-wave-example-dashboard.png" 40 | }, 41 | { 42 | "name": "Example Panel Edit", 43 | "path": "img/sine-wave-timeseries-panel-edit.png" 44 | } 45 | ], 46 | "version": "%VERSION%", 47 | "updated": "%TODAY%" 48 | }, 49 | "dependencies": { 50 | "grafanaDependency": ">=7.3.3", 51 | "plugins": [] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /selenium/configure.test.ts: -------------------------------------------------------------------------------- 1 | const { By, until } = require('selenium-webdriver'); 2 | 3 | import { getDriver, login, logHTMLOnFailure, saveTestState, GRAFANA_URL } from './helpers'; 4 | 5 | describe('configure', () => { 6 | jest.setTimeout(30000); 7 | let driver; 8 | let testStatus = { ok: true }; 9 | 10 | beforeAll(async () => { 11 | driver = await getDriver(); 12 | 13 | await login(driver); 14 | }); 15 | 16 | afterEach(async () => { 17 | await logHTMLOnFailure(testStatus, driver); 18 | testStatus.ok = true; 19 | }); 20 | 21 | afterAll(async () => { 22 | await driver.quit(); 23 | }); 24 | 25 | it( 26 | 'configures the plugin', 27 | saveTestState(testStatus, async () => { 28 | await driver.get(`${GRAFANA_URL}/datasources/new`); 29 | await driver.wait(until.elementLocated(By.css('div.add-data-source-category')), 5 * 1000); 30 | await driver.findElement(By.css("div.add-data-source-item[aria-label='Data source plugin item SQLite']")).click(); 31 | 32 | await driver.wait(until.elementLocated(By.css("input[placeholder='/path/to/the/database.db']")), 5 * 1000); 33 | await driver.findElement(By.css("input[placeholder='/path/to/the/database.db']")).sendKeys('/app/data.db'); 34 | 35 | await driver.findElement(By.xpath(`//*[text()[contains(translate(., "TS", "ts"), "save & test")]]`)).click(); 36 | 37 | await driver.wait(until.elementLocated(By.xpath(`//*[text()[contains(., "Data source is working")]]`)), 5 * 1000); 38 | }) 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /release_template.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | This is an official release of the Grafana SQLite plugin code. For some releases it takes a while 4 | to also appear in the Grafana repository, which is used to populate the plugins for the Grafana 5 | website as well as their grafana-cli. Some releases (especially pre-releases) from Github never 6 | appear in the Grafana repository. 7 | 8 | To install this specific release use one of the following methods. 9 | 10 | More details can also be found in the installation section of the [Readme](README.md). 11 | 12 | ## Installation 13 | 14 | ### Using the grafana-cli 15 | 16 | 1. Run this command: 17 | 18 | ```sh 19 | grafana-cli --pluginUrl https://github.com/fr-ser/grafana-sqlite-datasource/releases/download/v$VERSION/frser-sqlite-datasource-$VERSION.zip plugins install frser-sqlite-datasource 20 | ``` 21 | 22 | 2. See the installation instructions in the [Readme](README.md). 23 | 24 | ### Manual Installation 25 | 26 | 1. Download the [zip file](https://github.com/fr-ser/grafana-sqlite-datasource/releases/download/v$VERSION/frser-sqlite-datasource-$VERSION.zip) below 27 | 2. Extract the zip file into the data/plugins subdirectory for Grafana: `unzip -d /` 28 | 29 | Finding the plugin directory can sometimes be a challenge as this is platform and settings 30 | dependent. A common location for this on Linux devices is `/var/lib/grafana/plugins/` 31 | 3. See the installation instructions in the [Readme](README.md). 32 | 33 | ## Changelog 34 | 35 | For the full changelog refer to the [CHANGELOG.md](CHANGELOG.md). 36 | -------------------------------------------------------------------------------- /.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/get-started/set-up-development-environment#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/frser-sqlite-datasource/dist/gpx_sqlite-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_sqlite-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/frser-sqlite-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/frser-sqlite-datasource 41 | command=/bin/bash -c 'git config --global --add safe.directory /root/frser-sqlite-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 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | grafana: 5 | image: grafana/grafana:${GRAFANA_VERSION:-8.1.0} 6 | environment: 7 | TZ: Europe/Berlin 8 | # uncomment to install a comma separated list of plugins 9 | # GF_INSTALL_PLUGINS: grafana-worldmap-panel 10 | volumes: 11 | - './grafana_config/data.db:/app/data.db' 12 | - './grafana_config/grafana.ini:/etc/grafana/grafana.ini' 13 | - './grafana_config/datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yaml' 14 | - './grafana_config/dashboard.yaml:/etc/grafana/provisioning/dashboards/dashboard.yaml' 15 | - './grafana_config/dashboards/alert.json:/app/dashboards/alert.json' 16 | - './grafana_config/dashboards/graph_and_variables.json:/app/dashboards/graph_and_variables.json' 17 | - './grafana_config/dashboards/query_and_repitition.json:/app/dashboards/query_and_repitition.json' 18 | - './dist:/var/lib/grafana/plugins/frser-sqlite-datasource' 19 | ports: 20 | - 3000:3000 21 | depends_on: 22 | - postgres 23 | selenium: 24 | image: ${SELENIUM_IMAGE:-selenium/standalone-chrome:112.0} 25 | shm_size: 2gb 26 | ports: 27 | - 4444:4444 28 | - 5900:5900 # this is the VNC port. the password is "secret" 29 | start-setup: 30 | image: dadarek/wait-for-dependencies:0.2 31 | depends_on: 32 | - grafana 33 | - selenium 34 | command: grafana:3000 selenium:4444 35 | postgres: 36 | # used to compare the sqlite plugin 37 | image: postgres:12-alpine 38 | environment: 39 | - POSTGRES_USER=admin 40 | - POSTGRES_PASSWORD=changed_later 41 | - POSTGRES_DB=db_name 42 | -------------------------------------------------------------------------------- /src/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /selenium/helpers.ts: -------------------------------------------------------------------------------- 1 | const { By, Builder, error } = require('selenium-webdriver'); 2 | const chromeDriver = require('selenium-webdriver/chrome'); 3 | 4 | export const GRAFANA_URL = process.env.GRAFANA_URL || 'http://grafana:3000'; 5 | const SELENIUM_URL = process.env.SELENIUM_URL || 'localhost:4444'; 6 | 7 | export async function login(driver) { 8 | await driver.get(GRAFANA_URL); 9 | 10 | await driver.findElement(By.css("input[name='user']")).sendKeys('admin'); 11 | await driver.findElement(By.css("input[name='password']")).sendKeys('admin123'); 12 | await driver.findElement(By.css("button[aria-label='Login button']")).click(); 13 | await driver.wait(async () => { 14 | try { 15 | await driver.findElement(By.css("button[aria-label='Login button']")); 16 | } catch (err) { 17 | if (err instanceof error.NoSuchElementError) { 18 | return true; 19 | } 20 | } 21 | return false; 22 | }, 2 * 1000); 23 | } 24 | 25 | export async function getDriver() { 26 | return new Builder() 27 | .forBrowser('chrome') 28 | .setChromeOptions(new chromeDriver.Options()) 29 | .usingServer(`http://${SELENIUM_URL}/wd/hub`) 30 | .build(); 31 | } 32 | 33 | export function saveTestState(testStatus: { ok: boolean }, testFn: () => Promise) { 34 | return async function () { 35 | try { 36 | await testFn(); 37 | testStatus.ok = true; 38 | } catch (err) { 39 | testStatus.ok = false; 40 | throw err; 41 | } 42 | }; 43 | } 44 | 45 | export async function logHTMLOnFailure(testStatus: { ok: boolean }, driver: any) { 46 | if (testStatus.ok || process.env.VERBOSE_TEST_OUTPUT !== '1') { 47 | return; 48 | } 49 | 50 | let errorText: string; 51 | try { 52 | errorText = await driver.findElement(By.css('html')).getAttribute('innerHTML'); 53 | } catch (error) { 54 | errorText = error.toString(); 55 | } 56 | console.warn(errorText); 57 | } 58 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Plugin Installation 2 | 3 | The most up to date (but also most generic) information can always be found here: 4 | [Grafana Website - Plugin Installation](https://grafana.com/docs/grafana/latest/plugins/installation/#install-grafana-plugins) 5 | 6 | ## Recommended: Installing the Official and Released Plugin on an Existing Grafana With the CLI 7 | 8 | Grafana comes with a command line tool that can be used to install plugins. 9 | 10 | 1. Run this command: `grafana-cli plugins install frser-sqlite-datasource` 11 | 2. Restart the Grafana server. 12 | 3. To make sure the plugin was installed, check the list of installed data sources. Click the 13 | Plugins item in the main menu. Both core data sources and installed data sources will appear. 14 | 15 | ## Latest Version: Installing the newest Plugin Version on an Existing Grafana With the CLI 16 | 17 | The grafana-cli can also install plugins from a non-standard URL. This way even plugin versions, 18 | that are not (yet) released to the official Grafana repository can be installed. 19 | 20 | 1. Run this command: 21 | 22 | ```sh 23 | # replace the $VERSION part in the URL below with the desired version (e.g. 2.0.2) 24 | grafana-cli --pluginUrl https://github.com/fr-ser/grafana-sqlite-datasource/releases/download/v$VERSION/frser-sqlite-datasource-$VERSION.zip plugins install frser-sqlite-datasource 25 | ``` 26 | 27 | 2. See the recommended installation above (from the restart step) 28 | 29 | ## Manual: Installing the Plugin Manually on an Existing Grafana 30 | 31 | In case the grafana-cli does not work for whatever reason plugins can also be installed manually. 32 | 33 | 1. Get the zip file from [Latest release on Github](https://github.com/fr-ser/grafana-sqlite-datasource/releases/latest) 34 | 2. Extract the zip file into the data/plugins subdirectory for Grafana: 35 | `unzip -d /` 36 | 37 | Finding the plugin directory can sometimes be a challenge as this is platform and settings 38 | dependent. A common location for this on Linux devices is `/var/lib/grafana/plugins/` 39 | 3. See the recommended installation above (from the restart step) 40 | -------------------------------------------------------------------------------- /.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.ts'; 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 | function loadJson(path: string) { 25 | const rawJson = fs.readFileSync(path, 'utf8'); 26 | return JSON.parse(rawJson); 27 | } 28 | 29 | export function getPackageJson() { 30 | return loadJson(path.resolve(process.cwd(), 'package.json')); 31 | } 32 | 33 | export function getPluginJson() { 34 | return loadJson(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); 35 | } 36 | 37 | export function getCPConfigVersion() { 38 | const cprcJson = path.resolve(process.cwd(), './.config', '.cprc.json'); 39 | return fs.existsSync(cprcJson) ? loadJson(cprcJson).version : { version: 'unknown' }; 40 | } 41 | 42 | export function hasReadme() { 43 | return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); 44 | } 45 | 46 | // Support bundling nested plugins by finding all plugin.json files in src directory 47 | // then checking for a sibling module.[jt]sx? file. 48 | export async function getEntries() { 49 | const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); 50 | 51 | const plugins = await Promise.all( 52 | pluginsJson.map((pluginJson) => { 53 | const folder = path.dirname(pluginJson); 54 | return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); 55 | }) 56 | ); 57 | 58 | return plugins.reduce>((result, modules) => { 59 | return modules.reduce((innerResult, module) => { 60 | const pluginPath = path.dirname(module); 61 | const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); 62 | const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; 63 | 64 | innerResult[entryName] = module; 65 | return innerResult; 66 | }, result); 67 | }, {}); 68 | } 69 | -------------------------------------------------------------------------------- /src/DataSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInstanceSettings, DataFrame, ScopedVars } from '@grafana/data'; 2 | import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; 3 | import { MyDataSourceOptions, SQLiteQuery } from './types'; 4 | 5 | export class DataSource extends DataSourceWithBackend { 6 | templateSrv; 7 | 8 | constructor(instanceSettings: DataSourceInstanceSettings) { 9 | super(instanceSettings); 10 | 11 | this.templateSrv = getTemplateSrv(); 12 | this.annotations = {}; 13 | } 14 | 15 | applyTemplateVariables(query: SQLiteQuery, scopedVars: ScopedVars): SQLiteQuery { 16 | query.queryText = this.templateSrv.replace(query.rawQueryText, scopedVars); 17 | return query; 18 | } 19 | 20 | async metricFindQuery(query: string, options?: any) { 21 | if (!query) { 22 | return []; 23 | } 24 | const response = await this.query({ 25 | targets: [ 26 | { 27 | refId: 'metricFindQuery', 28 | rawQueryText: query, 29 | queryText: query, 30 | timeColumns: [], 31 | }, 32 | ], 33 | } as any).toPromise(); 34 | 35 | if (response === undefined) { 36 | throw new Error('Received no response at all'); 37 | } else if (response.error) { 38 | throw new Error(response.error.message); 39 | } 40 | 41 | const data = response.data[0] as DataFrame; 42 | if (data.fields.length === 1) { 43 | return data.fields[0].values.toArray().map((text) => ({ text })); 44 | } else if (data.fields.length === 2) { 45 | const textIndex = data.fields.findIndex((x) => x.name === '__text'); 46 | const valueIndex = data.fields.findIndex((x) => x.name === '__value'); 47 | if (textIndex === -1 || valueIndex === -1) { 48 | throw new Error( 49 | `No columns named "__text" and "__value" were found. Columns: ${data.fields.map((x) => x.name).join(',')}` 50 | ); 51 | } 52 | 53 | const valueArray = data.fields[valueIndex].values.toArray(); 54 | return data.fields[textIndex].values.toArray().map((text, index) => ({ text, value: valueArray[index] })); 55 | } else { 56 | throw new Error( 57 | `Received more than two (${data.fields.length}) fields: ${data.fields.map((x) => x.name).join(',')}` 58 | ); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /selenium/alerting.test.ts: -------------------------------------------------------------------------------- 1 | const { By, until } = require('selenium-webdriver'); 2 | 3 | import { getDriver, login, saveTestState, logHTMLOnFailure, GRAFANA_URL } from './helpers'; 4 | 5 | describe('alerting', () => { 6 | jest.setTimeout(30000); 7 | let driver; 8 | let testStatus = { ok: true }; 9 | 10 | beforeAll(async () => { 11 | driver = await getDriver(); 12 | await login(driver); 13 | }); 14 | 15 | afterEach(async () => { 16 | await logHTMLOnFailure(testStatus, driver); 17 | testStatus.ok = true; 18 | }); 19 | 20 | afterAll(async () => { 21 | await driver.quit(); 22 | }); 23 | 24 | it( 25 | 'passes the manual alert test with no data', 26 | saveTestState(testStatus, async () => { 27 | await driver.get(`${GRAFANA_URL}/d/y7EuI6m7z/alert-test?tab=alert&editPanel=2`); 28 | await driver.wait(until.elementLocated(By.xpath(`//button//span[text()[contains(., "Test rule")]]`)), 5 * 1000); 29 | await driver 30 | .findElement(By.xpath(`//button//span[text()[contains(., "Test rule")]]`)) 31 | .findElement(By.xpath('./..')) 32 | .click(); 33 | 34 | await driver.wait( 35 | until.elementLocated( 36 | By.xpath(`//div[contains(@class, 'json-formatter-row')]//span[text()[contains(., "no_data")]]`) 37 | ), 38 | 5 * 1000 39 | ); 40 | await driver.findElement( 41 | By.xpath(`//div[contains(@class, 'json-formatter-row')]//span[text()[contains(., "false = false")]]`) 42 | ); 43 | }) 44 | ); 45 | 46 | it( 47 | 'passes the manual alert test with data', 48 | saveTestState(testStatus, async () => { 49 | await driver.get(`${GRAFANA_URL}/d/y7EuI6m7z/alert-test?tab=alert&editPanel=3`); 50 | await driver.wait(until.elementLocated(By.xpath(`//button//span[text()[contains(., "Test rule")]]`)), 5 * 1000); 51 | await driver 52 | .findElement(By.xpath(`//button//span[text()[contains(., "Test rule")]]`)) 53 | .findElement(By.xpath('./..')) 54 | .click(); 55 | 56 | await driver.wait( 57 | until.elementLocated( 58 | By.xpath(`//div[contains(@class, 'json-formatter-row')]//span[text()[contains(., "pending")]]`) 59 | ), 60 | 5 * 1000 61 | ); 62 | await driver.findElement( 63 | By.xpath(`//div[contains(@class, 'json-formatter-row')]//span[text()[contains(., "true = true")]]`) 64 | ); 65 | }) 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /src/test/template_srv.ts: -------------------------------------------------------------------------------- 1 | // copied from: https://github.com/grafana/grafana/blob/37e2becdd71b350e4c1f82416844d900f23fd0ce/public/app/features/templating/template_srv.mock.ts 2 | 3 | import { ScopedVars, TimeRange, TypedVariableModel } from '@grafana/data'; 4 | import { TemplateSrv } from '@grafana/runtime'; 5 | 6 | export const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; 7 | 8 | /** 9 | * Mock for TemplateSrv where you can just supply map of key and values and it will do the interpolation based on that. 10 | * For simple tests whether you your data source for example calls correct replacing code. 11 | * 12 | * This is implementing TemplateSrv interface but that is not enough in most cases. Datasources require some additional 13 | * methods and usually require TemplateSrv class directly instead of just the interface which probably should be fixed 14 | * later on. 15 | */ 16 | export class TemplateSrvMock implements TemplateSrv { 17 | private regex = variableRegex; 18 | constructor(private variables: Record) {} 19 | 20 | getVariables(): TypedVariableModel[] { 21 | return Object.keys(this.variables).map((key) => { 22 | return { 23 | type: 'custom', 24 | name: key, 25 | label: key, 26 | }; 27 | // TODO: we remove this type assertion in a later PR 28 | // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 29 | }) as TypedVariableModel[]; 30 | } 31 | 32 | replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { 33 | if (!target) { 34 | return target ?? ''; 35 | } 36 | 37 | this.regex.lastIndex = 0; 38 | 39 | return target.replace(this.regex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => { 40 | const variableName = var1 || var2 || var3; 41 | return this.variables[variableName]; 42 | }); 43 | } 44 | 45 | getVariableName(expression: string) { 46 | this.regex.lastIndex = 0; 47 | const match = this.regex.exec(expression); 48 | if (!match) { 49 | return null; 50 | } 51 | return match.slice(1).find((match) => match !== undefined); 52 | } 53 | 54 | containsTemplate(target: string | undefined): boolean { 55 | if (!target) { 56 | return false; 57 | } 58 | 59 | this.regex.lastIndex = 0; 60 | const match = this.regex.exec(target); 61 | return match !== null; 62 | } 63 | 64 | updateTimeRange(timeRange: TimeRange) {} 65 | } 66 | -------------------------------------------------------------------------------- /selenium/query_and_repetition.test.ts: -------------------------------------------------------------------------------- 1 | const { By, until } = require('selenium-webdriver'); 2 | 3 | import { getDriver, login, logHTMLOnFailure, saveTestState, GRAFANA_URL } from './helpers'; 4 | 5 | describe('query variables and repetition', () => { 6 | jest.setTimeout(30000); 7 | let driver; 8 | let testStatus = { ok: true }; 9 | 10 | beforeAll(async () => { 11 | driver = await getDriver(); 12 | 13 | await login(driver); 14 | await driver.get(`${GRAFANA_URL}/d/jng4Dei7k/query-variables-and-repetition`); 15 | await driver.wait( 16 | until.elementLocated(By.xpath(`//a[text()[contains(., "Query Variables and Repetition")]]`)), 17 | 5 * 1000 18 | ); 19 | }); 20 | 21 | afterEach(async () => { 22 | await logHTMLOnFailure(testStatus, driver); 23 | testStatus.ok = true; 24 | }); 25 | 26 | afterAll(async () => { 27 | await driver.quit(); 28 | }); 29 | 30 | it( 31 | 'shows a panel per variable', 32 | saveTestState(testStatus, async () => { 33 | const v7_3_panel_aria_label = `//div[contains(@aria-label, 'container title $cities')]`; 34 | const v8_1_panel_aria_label = `//section[contains(@aria-label, '$cities panel')]`; 35 | 36 | let cityPanels = await driver.findElements(By.xpath(`(${v7_3_panel_aria_label} | ${v8_1_panel_aria_label})`)); 37 | expect(cityPanels).toHaveLength(3); 38 | 39 | await driver 40 | .findElement( 41 | By.xpath(`//div[contains(@class, 'submenu-item gf-form-inline')]//label[text()[contains(., "Cities")]]`) 42 | ) 43 | .findElement(By.xpath('./..')) 44 | .click(); 45 | await driver 46 | .findElement( 47 | By.xpath(`//div[contains(@class, 'submenu-item gf-form-inline')]//span[text()[contains(., "London")]]`) 48 | ) 49 | .click(); 50 | await driver.findElement(By.xpath(`//div[contains(@class, 'refresh-picker')]//button`)).click(); 51 | 52 | cityPanels = await driver.findElements(By.xpath(`(${v7_3_panel_aria_label} | ${v8_1_panel_aria_label})`)); 53 | }) 54 | ); 55 | 56 | it( 57 | 'shows annotations', 58 | saveTestState(testStatus, async () => { 59 | const annotationMarker = await driver.findElement( 60 | By.css(`div.graph-panel__chart div.events_line.flot-temp-elem div`) 61 | ); 62 | await driver.actions({ async: true }).move({ origin: annotationMarker }).perform(); 63 | await driver.findElement(By.css(`div.drop-popover--annotation`)); 64 | }) 65 | ); 66 | }); 67 | -------------------------------------------------------------------------------- /pkg/plugin/macros.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 10 | ) 11 | 12 | const macroRegex = `\$__([_a-zA-Z0-9]+)\(([^\)]*)\)` 13 | 14 | func applyMacros(queryConfig *queryConfigStruct) error { 15 | compiledRegex, err := regexp.Compile(macroRegex) 16 | if err != nil { 17 | log.DefaultLogger.Error("Could create macro regex", "err", err) 18 | return err 19 | } 20 | 21 | newQuery := "" 22 | lastReplacedIndex := 0 23 | 24 | for _, match := range compiledRegex.FindAllSubmatchIndex([]byte(queryConfig.FinalQuery), -1) { 25 | groups := []string{} 26 | 27 | for i := 0; i < len(match); i += 2 { 28 | groups = append(groups, queryConfig.FinalQuery[match[i]:match[i+1]]) 29 | } 30 | 31 | var replacedString string 32 | switch groups[1] { 33 | case "unixEpochGroupSeconds": 34 | replacedString, err = unixEpochGroupSeconds(queryConfig, strings.Split(groups[2], ",")) 35 | if err != nil { 36 | return err 37 | } 38 | default: 39 | replacedString = groups[0] 40 | } 41 | 42 | newQuery += queryConfig.FinalQuery[lastReplacedIndex:match[0]] + replacedString 43 | lastReplacedIndex = match[1] 44 | } 45 | 46 | queryConfig.FinalQuery = newQuery + queryConfig.FinalQuery[lastReplacedIndex:] 47 | 48 | return nil 49 | } 50 | 51 | func unixEpochGroupSeconds(queryConfig *queryConfigStruct, arguments []string) (string, error) { 52 | if len(arguments) < 2 || len(arguments) > 3 { 53 | return "", fmt.Errorf( 54 | "unsupported number of arguments (%d) for unixEpochGroupSeconds", len(arguments), 55 | ) 56 | } 57 | var err error 58 | queryConfig.FillInterval, err = strconv.Atoi(strings.Trim(arguments[1], " ")) 59 | if err != nil { 60 | log.DefaultLogger.Error( 61 | "Could not convert grouping interval to an integer", 62 | "macro", 63 | "unixEpochGroupSeconds", 64 | "err", 65 | err, 66 | ) 67 | return "", fmt.Errorf( 68 | "could not convert '%s' to an integer grouping interval", arguments[1], 69 | ) 70 | } 71 | 72 | // the gap filling value 73 | if len(arguments) == 3 { 74 | if strings.ToLower(strings.Trim(arguments[2], " ")) != "null" { 75 | return "", fmt.Errorf("unsupported gap filling value of: `%s`", arguments[2]) 76 | } 77 | queryConfig.ShouldFillValues = true 78 | } 79 | 80 | return fmt.Sprintf( 81 | "cast((%s / %d) as int) * %d", 82 | arguments[0], 83 | queryConfig.FillInterval, 84 | queryConfig.FillInterval, 85 | ), nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/plugin/check_health.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/grafana/grafana-plugin-sdk-go/backend" 12 | ) 13 | 14 | func IsPathBlocked(path string) bool { 15 | blockList, exists := os.LookupEnv("GF_PLUGIN_BLOCK_LIST") 16 | if !exists || blockList == "" { 17 | return false 18 | } 19 | 20 | blockedTerms := strings.Split(blockList, ",") 21 | for _, term := range blockedTerms { 22 | term = strings.TrimSpace(term) 23 | if term != "" && strings.Contains(path, term) { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | func checkDB(pathPrefix string, path string, options string) error { 31 | if IsPathBlocked(path) { 32 | return fmt.Errorf("path contains blocked term from GF_PLUGIN_BLOCK_LIST") 33 | } 34 | 35 | if pathPrefix == "file:" || pathPrefix == "" { 36 | fileInfo, err := os.Stat(path) 37 | if errors.Is(err, os.ErrNotExist) { 38 | return fmt.Errorf("no file exists at the file path") 39 | } else if err != nil { 40 | return fmt.Errorf("error checking path: %v", err) 41 | } else if fileInfo.IsDir() { 42 | return fmt.Errorf("the provided path is a directory instead of a file") 43 | } 44 | } 45 | 46 | db, err := sql.Open("sqlite", pathPrefix+path+"?"+options) 47 | if err != nil { 48 | return fmt.Errorf("error opening %s%s: %v", pathPrefix, path, err) 49 | } 50 | 51 | _, err = db.Exec("pragma schema_version;") 52 | if err != nil { 53 | return fmt.Errorf("error checking for valid SQLite file: %v", err) 54 | } 55 | 56 | err = db.Close() 57 | if err != nil { 58 | return fmt.Errorf("error closing database file: %v", err) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // CheckHealth handles health checks sent from Grafana to the plugin. 65 | // The main use case for these health checks is the test button on the 66 | // datasource configuration page which allows users to verify that 67 | // a datasource is working as expected. 68 | func (ds *sqliteDatasource) CheckHealth(ctx context.Context, _ *backend.CheckHealthRequest) ( 69 | *backend.CheckHealthResult, error, 70 | ) { 71 | err := checkDB(ds.pluginConfig.PathPrefix, ds.pluginConfig.Path, ds.pluginConfig.PathOptions) 72 | if err != nil { 73 | return &backend.CheckHealthResult{ 74 | Status: backend.HealthStatusError, 75 | Message: fmt.Sprintf("error checking db: %s", err), 76 | }, nil 77 | } 78 | 79 | return &backend.CheckHealthResult{ 80 | Status: backend.HealthStatusOk, 81 | Message: "Data source is working", 82 | }, nil 83 | } 84 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Grafana SQLite Datasource 2 | 3 | This is a Grafana backend plugin to allow using a SQLite database as a data source. 4 | 5 | The plugin was built using the grafana plugin sdk and npx grafana toolkit. Information can be 6 | found at: 7 | 8 | - 9 | - 10 | - 11 | 12 | ## Getting started 13 | 14 | This project uses the `Makefile` as the main script tool for development tasks. Run `make help` to 15 | get an overview of the available commands. 16 | 17 | ### Requirements 18 | 19 | - nodejs 20 | - go 21 | - docker and docker compose 22 | - make 23 | 24 | ### (First Time) Installation 25 | 26 | ```sh 27 | # installing packages 28 | make install 29 | # optional: using git hooks 30 | make add-git-hook 31 | ``` 32 | 33 | ### Start up Grafana 34 | 35 | ```sh 36 | make build # this build the frontend and backend 37 | make start # credentials admin / admin123 38 | ``` 39 | 40 | ## Testing 41 | 42 | ```sh 43 | make test 44 | ``` 45 | 46 | ### Quick e2e tests with Selenium 47 | 48 | First start the docker environment with `make selenium-test`. This will also run the tests. 49 | Regardless of the tests passing the environment will stay up and running. 50 | 51 | Now you can connect to the dockerized browser via a `VNC` client/viewer. 52 | 53 | The VNC password is `secret`. 54 | 55 | **Note - Macos M1/AMD support:** The selenium image does not support arm architectures yet. 56 | 57 | In order to run the tests on an ARM architecture please change the docker image of selenium by using this environment variable: 58 | `SELENIUM_IMAGE=seleniarm/standalone-chromium:112.0`. 59 | 60 | You can find more information here: 61 | 62 | #### VNC Viewer 63 | 64 | On linux distributions you can use remmina as a VNC viewer. 65 | 66 | On MacOs you can use the preinstalled "screen sharing" application as a VNC viewer. 67 | 68 | ## Release process 69 | 70 | After step 3 Github Actions should take over and create a new release. 71 | Steps 4 and 5 are for publishing the release to Grafana repository. 72 | 73 | 1. Make sure a section in the Changelog exists with `## [Unreleased]` 74 | 2. Push the changes and merge to the default branch 75 | 3. Get the md5 hash of the release from the Github Action or from the release page (text file) 76 | 4. Within the Grafana Cloud account a request for a plugin update can be started: 77 | 78 | -------------------------------------------------------------------------------- /selenium/writing_queries.test.ts: -------------------------------------------------------------------------------- 1 | const { By, until, Key } = require('selenium-webdriver'); 2 | 3 | import { getDriver, login, logHTMLOnFailure, saveTestState, GRAFANA_URL } from './helpers'; 4 | 5 | describe.only('writing queries', () => { 6 | jest.setTimeout(30000); 7 | let driver; 8 | let testStatus = { ok: true }; 9 | 10 | beforeAll(async () => { 11 | driver = await getDriver(); 12 | 13 | await login(driver); 14 | await driver.get(`${GRAFANA_URL}/explore`); 15 | await driver.wait(until.elementLocated(By.css('.monaco-editor')), 5 * 1000); 16 | }); 17 | 18 | afterEach(async () => { 19 | await logHTMLOnFailure(testStatus, driver); 20 | testStatus.ok = true; 21 | }); 22 | 23 | afterAll(async () => { 24 | await driver.quit(); 25 | }); 26 | 27 | it( 28 | 'runs an updated query', 29 | saveTestState(testStatus, async () => { 30 | // the .inputarea element is an invisible accessibility element belonging to the monaco code editor 31 | await driver.findElement(By.css('.inputarea')).sendKeys(Key.chord(Key.CONTROL, 'a'), 'SELECT 12345678987654321'); 32 | await driver.findElement(By.css('.explore-toolbar')).click(); 33 | 34 | // check that the query was executed with the new input 35 | await driver.wait( 36 | until.elementLocated( 37 | By.xpath(`//div[contains(@aria-label, 'Explore Table')]//*[text()[contains(., "12345678987654321")]]`) 38 | ), 39 | 5 * 1000 40 | ); 41 | }) 42 | ); 43 | 44 | it( 45 | 'converts the new code editor to the legacy code editor', 46 | saveTestState(testStatus, async () => { 47 | // the .inputarea element is an invisible accessibility element belonging to the monaco code editor 48 | await driver.findElement(By.css('.inputarea')).sendKeys(Key.chord(Key.CONTROL, 'a'), 'SELECT 12121992'); 49 | await driver.findElement(By.xpath(`//input[contains(@role, 'use-legacy-editor-switch')]//..//label`)).click(); 50 | await driver.wait( 51 | until.elementLocated( 52 | By.xpath(`//textarea[contains(@role, 'query-editor-input')][text()[contains(., "12121992")]]`) 53 | ), 54 | 5 * 1000 55 | ); 56 | 57 | await driver 58 | .findElement(By.css('[role="query-editor-input"]')) 59 | .sendKeys(Key.chord(Key.CONTROL, 'a'), 'SELECT 231992'); 60 | 61 | // check that the query was executed with the new input 62 | await driver.wait( 63 | until.elementLocated( 64 | By.xpath(`//div[contains(@aria-label, 'Explore Table')]//*[text()[contains(., "231992")]]`) 65 | ), 66 | 5 * 1000 67 | ); 68 | }) 69 | ); 70 | }); 71 | -------------------------------------------------------------------------------- /pkg/plugin/sqlite_datasource.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" 10 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 11 | 12 | // register sqlite driver 13 | _ "modernc.org/sqlite" 14 | ) 15 | 16 | // Make sure sqliteDatasource implements required interfaces. This is important to do 17 | // since otherwise we will only get a not implemented error response from plugin in 18 | // runtime. 19 | var ( 20 | _ backend.QueryDataHandler = (*sqliteDatasource)(nil) 21 | _ backend.CheckHealthHandler = (*sqliteDatasource)(nil) 22 | ) 23 | 24 | type sqliteDatasource struct { 25 | pluginConfig pluginConfig 26 | } 27 | 28 | type pluginConfig struct { 29 | Path string 30 | PathOptions string 31 | PathPrefix string 32 | AttachLimit *int64 33 | } 34 | 35 | // NewDataSource creates a new datasource instance. 36 | func NewDataSource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { 37 | log.DefaultLogger.Info("Creating instance") 38 | var config pluginConfig 39 | 40 | err := json.Unmarshal(settings.JSONData, &config) 41 | if err != nil { 42 | log.DefaultLogger.Error("Could unmarshal settings of data source", "err", err) 43 | return &sqliteDatasource{}, fmt.Errorf("error while unmarshalling data source settings: %s", err) 44 | } 45 | 46 | securePathOptions, securePathOptionsExist := settings.DecryptedSecureJSONData["securePathOptions"] 47 | if securePathOptionsExist { 48 | if config.PathOptions == "" { 49 | config.PathOptions = securePathOptions 50 | } else { 51 | config.PathOptions = config.PathOptions + "&" + securePathOptions 52 | } 53 | } 54 | 55 | return &sqliteDatasource{pluginConfig: config}, nil 56 | } 57 | 58 | // QueryData handles multiple queries and returns multiple responses. 59 | // req contains the queries []DataQuery (where each query contains RefID as a unique identifier). 60 | // The QueryDataResponse contains a map of RefID to the response for each query, and each response 61 | // contains Frames ([]*Frame). 62 | func (ds *sqliteDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) ( 63 | *backend.QueryDataResponse, error, 64 | ) { 65 | log.DefaultLogger.Debug("Received request for data") 66 | response := backend.NewQueryDataResponse() 67 | 68 | // loop over queries and execute them individually. 69 | for _, q := range req.Queries { 70 | response.Responses[q.RefID] = query(q, ds.pluginConfig, ctx) 71 | log.DefaultLogger.Debug("Finished query", "refID", q.RefID) 72 | } 73 | 74 | return response, nil 75 | } 76 | -------------------------------------------------------------------------------- /.config/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG grafana_version=latest 2 | ARG grafana_image=grafana-enterprise 3 | 4 | FROM grafana/${grafana_image}:${grafana_version} 5 | 6 | ARG anonymous_auth_enabled=true 7 | ARG development=false 8 | ARG TARGETARCH 9 | 10 | ARG GO_VERSION=1.21.6 11 | ARG GO_ARCH=${TARGETARCH:-amd64} 12 | 13 | ENV DEV "${development}" 14 | 15 | # Make it as simple as possible to access the grafana instance for development purposes 16 | # Do NOT enable these settings in a public facing / production grafana instance 17 | ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" 18 | ENV GF_AUTH_ANONYMOUS_ENABLED "${anonymous_auth_enabled}" 19 | ENV GF_AUTH_BASIC_ENABLED "false" 20 | # Set development mode so plugins can be loaded without the need to sign 21 | ENV GF_DEFAULT_APP_MODE "development" 22 | 23 | 24 | LABEL maintainer="Grafana Labs " 25 | 26 | ENV GF_PATHS_HOME="/usr/share/grafana" 27 | WORKDIR $GF_PATHS_HOME 28 | 29 | USER root 30 | 31 | # Installing supervisor and inotify-tools 32 | RUN if [ "${development}" = "true" ]; then \ 33 | if grep -i -q alpine /etc/issue; then \ 34 | apk add supervisor inotify-tools git; \ 35 | elif grep -i -q ubuntu /etc/issue; then \ 36 | DEBIAN_FRONTEND=noninteractive && \ 37 | apt-get update && \ 38 | apt-get install -y supervisor inotify-tools git && \ 39 | rm -rf /var/lib/apt/lists/*; \ 40 | else \ 41 | echo 'ERROR: Unsupported base image' && /bin/false; \ 42 | fi \ 43 | fi 44 | 45 | COPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini 46 | COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf 47 | 48 | 49 | # Installing Go 50 | RUN if [ "${development}" = "true" ]; then \ 51 | curl -O -L https://golang.org/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ 52 | rm -rf /usr/local/go && \ 53 | tar -C /usr/local -xzf go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ 54 | echo "export PATH=$PATH:/usr/local/go/bin:~/go/bin" >> ~/.bashrc && \ 55 | rm -f go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \ 56 | fi 57 | 58 | # Installing delve for debugging 59 | RUN if [ "${development}" = "true" ]; then \ 60 | /usr/local/go/bin/go install github.com/go-delve/delve/cmd/dlv@latest; \ 61 | fi 62 | 63 | # Installing mage for plugin (re)building 64 | RUN if [ "${development}" = "true" ]; then \ 65 | git clone https://github.com/magefile/mage; \ 66 | cd mage; \ 67 | export PATH=$PATH:/usr/local/go/bin; \ 68 | go run bootstrap.go; \ 69 | fi 70 | 71 | # Inject livereload script into grafana index.html 72 | RUN sed -i 's|||g' /usr/share/grafana/public/views/index.html 73 | 74 | 75 | COPY entrypoint.sh /entrypoint.sh 76 | RUN chmod +x /entrypoint.sh 77 | ENTRYPOINT ["/entrypoint.sh"] 78 | -------------------------------------------------------------------------------- /.config/types/webpack-plugins.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'replace-in-file-webpack-plugin' { 2 | import { Compiler, Plugin } from 'webpack'; 3 | 4 | interface ReplaceRule { 5 | search: string | RegExp; 6 | replace: string | ((match: string) => string); 7 | } 8 | 9 | interface ReplaceOption { 10 | dir?: string; 11 | files?: string[]; 12 | test?: RegExp | RegExp[]; 13 | rules: ReplaceRule[]; 14 | } 15 | 16 | class ReplaceInFilePlugin extends Plugin { 17 | constructor(options?: ReplaceOption[]); 18 | options: ReplaceOption[]; 19 | apply(compiler: Compiler): void; 20 | } 21 | 22 | export = ReplaceInFilePlugin; 23 | } 24 | 25 | declare module 'webpack-livereload-plugin' { 26 | import { ServerOptions } from 'https'; 27 | import { Compiler, Plugin, Stats, Compilation } from 'webpack'; 28 | 29 | interface Options extends Pick { 30 | /** 31 | * protocol for livereload `