diff --git a/.github/workflows/grafana-plugin.yml b/.github/workflows/grafana-plugin.yml index 2a354efd..1c755432 100644 --- a/.github/workflows/grafana-plugin.yml +++ b/.github/workflows/grafana-plugin.yml @@ -25,45 +25,126 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + # The Node version comes from the plugin's .nvmrc so it cannot drift away + # from the toolchain again; npm caching is keyed on the lockfile. - name: Setup Node.js environment uses: actions/setup-node@v4 with: - node-version: "14.x" + node-version-file: connectors/grafana-plugin/.nvmrc + cache: 'npm' + cache-dependency-path: connectors/grafana-plugin/package-lock.json - name: Setup Go environment uses: actions/setup-go@v5 with: go-version: "1.21" - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + - name: Install dependencies and Build and test frontend + run: | + cd connectors/grafana-plugin/ + npm ci + npm run build + + - name: Install dependencies and Build backend + run: | + cd connectors/grafana-plugin/ + ./backend-compile.sh + + # The job above exercises the npm path directly. This one exercises the Maven + # entrypoint (`mvn -Pwith-grafana-plugin`), which drives the same frontend build + # through frontend-maven-plugin. Without it that path is unverified: the module is + # not in the `with-all-connectors` profile that compile-check.yml builds, and the + # Jenkinsfile's bare `mvn clean install` does not activate `with-grafana-plugin` + # either -- so a toolchain change can leave the Maven build broken while every + # check is green. + maven-build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Cache yarn cache - uses: actions/cache@v4 - id: cache-yarn-cache + - name: Setup JDK + uses: actions/setup-java@v4 with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Cache node_modules - id: cache-node-modules - uses: actions/cache@v4 + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + - name: Setup Go environment + uses: actions/setup-go@v5 with: - path: node_modules - key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- + go-version: "1.21" - - name: Install dependencies and Build and test frontend + - name: Build through the Maven entrypoint + run: mvn clean package -Pwith-grafana-plugin -DskipTests -ntp + + # plugin.json declares a grafanaDependency floor, but the @grafana/* packages are + # webpack externals resolved from the host at runtime -- so a plugin built against a + # newer Grafana compiles cleanly and then fails inside an older one. This job boots + # the exact version parsed out of grafanaDependency and checks that the plugin loads + # and answers a table-model query there. Changing the declared floor changes what is + # tested; nothing here needs editing. + minimum-grafana-version: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version-file: connectors/grafana-plugin/.nvmrc + cache: 'npm' + cache-dependency-path: connectors/grafana-plugin/package-lock.json + + - name: Setup Go environment + uses: actions/setup-go@v5 + with: + go-version: "1.21" + + - name: Resolve the declared minimum Grafana version + id: floor run: | cd connectors/grafana-plugin/ - yarn install --frozen-lockfile - yarn build + version=$(node -e " + const dep = require('./src/plugin.json').dependencies.grafanaDependency; + const m = /(\d+\.\d+\.\d+)/.exec(dep); + if (!m) { console.error('cannot parse grafanaDependency: ' + dep); process.exit(1); } + process.stdout.write(m[1]); + ") + echo "Declared floor: $version" + echo "version=$version" >> "$GITHUB_OUTPUT" - - name: Install dependencies and Build backend + - name: Build the plugin run: | cd connectors/grafana-plugin/ + npm ci + npm run build ./backend-compile.sh + + - name: Boot Grafana at the declared minimum and wait for both services + env: + GRAFANA_VERSION: ${{ steps.floor.outputs.version }} + GRAFANA_IMAGE: grafana + run: | + cd connectors/grafana-plugin/ + docker compose up -d --build + # IoTDB's REST port answers before the native RPC port is ready, so wait on + # Grafana's health endpoint and give the datasource a moment to provision. + for i in $(seq 1 60); do + if curl -sf http://localhost:3000/api/health > /dev/null; then break; fi + sleep 5 + done + curl -sf http://localhost:3000/api/health + + - name: Run the compatibility smoke test + run: | + cd connectors/grafana-plugin/ + npx playwright install --with-deps chromium + npm run e2e -- tests/minimumGrafanaVersion.spec.ts + + - name: Dump service logs on failure + if: failure() + run: | + cd connectors/grafana-plugin/ + docker compose logs --no-color --tail 200 diff --git a/connectors/grafana-plugin/.config/.cprc.json b/connectors/grafana-plugin/.config/.cprc.json new file mode 100644 index 00000000..23cda165 --- /dev/null +++ b/connectors/grafana-plugin/.config/.cprc.json @@ -0,0 +1,3 @@ +{ + "version": "7.9.0" +} diff --git a/connectors/grafana-plugin/.config/.prettierrc.js b/connectors/grafana-plugin/.config/.prettierrc.js new file mode 100644 index 00000000..bf506f5c --- /dev/null +++ b/connectors/grafana-plugin/.config/.prettierrc.js @@ -0,0 +1,16 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in .config/README.md + */ + +module.exports = { + endOfLine: 'auto', + printWidth: 120, + trailingComma: 'es5', + semi: true, + jsxSingleQuote: false, + singleQuote: true, + useTabs: false, + tabWidth: 2, +}; diff --git a/connectors/grafana-plugin/.config/Dockerfile b/connectors/grafana-plugin/.config/Dockerfile new file mode 100644 index 00000000..0c5e6e8e --- /dev/null +++ b/connectors/grafana-plugin/.config/Dockerfile @@ -0,0 +1,77 @@ +ARG grafana_version=latest +ARG grafana_image=grafana-enterprise + +FROM grafana/${grafana_image}:${grafana_version} + +ARG anonymous_auth_enabled=true +ARG development=false +ARG TARGETARCH + +ARG GO_VERSION=1.21.6 +ARG GO_ARCH=${TARGETARCH:-amd64} + +ENV DEV "${development}" + +# Make it as simple as possible to access the grafana instance for development purposes +# Do NOT enable these settings in a public facing / production grafana instance +ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" +ENV GF_AUTH_ANONYMOUS_ENABLED "${anonymous_auth_enabled}" +ENV GF_AUTH_BASIC_ENABLED "false" +# Set development mode so plugins can be loaded without the need to sign +ENV GF_DEFAULT_APP_MODE "development" + + +LABEL maintainer="Grafana Labs " + +ENV GF_PATHS_HOME="/usr/share/grafana" +WORKDIR $GF_PATHS_HOME + +USER root + +# Installing supervisor and inotify-tools +RUN if [ "${development}" = "true" ]; then \ + if grep -i -q alpine /etc/issue; then \ + apk add supervisor inotify-tools git; \ + elif grep -i -q ubuntu /etc/issue; then \ + DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get install -y supervisor inotify-tools git && \ + rm -rf /var/lib/apt/lists/*; \ + else \ + echo 'ERROR: Unsupported base image' && /bin/false; \ + fi \ + fi + +COPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini +COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + + +# Installing Go +RUN if [ "${development}" = "true" ]; then \ + curl -O -L https://golang.org/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ + rm -rf /usr/local/go && \ + tar -C /usr/local -xzf go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \ + echo "export PATH=$PATH:/usr/local/go/bin:~/go/bin" >> ~/.bashrc && \ + rm -f go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \ + fi + +# Installing delve for debugging +RUN if [ "${development}" = "true" ]; then \ + /usr/local/go/bin/go install github.com/go-delve/delve/cmd/dlv@latest; \ + fi + +# Installing mage for plugin (re)building +RUN if [ "${development}" = "true" ]; then \ + git clone https://github.com/magefile/mage; \ + cd mage; \ + export PATH=$PATH:/usr/local/go/bin; \ + go run bootstrap.go; \ + fi + +# Inject livereload script into grafana index.html +RUN sed -i 's|||g' /usr/share/grafana/public/views/index.html + + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/connectors/grafana-plugin/.config/README.md b/connectors/grafana-plugin/.config/README.md new file mode 100644 index 00000000..1ec4b41e --- /dev/null +++ b/connectors/grafana-plugin/.config/README.md @@ -0,0 +1,176 @@ +# Default build configuration by Grafana + +**This is an auto-generated directory and is not intended to be changed! ⚠️** + +The `.config/` directory holds basic configuration for the different tools +that are used to develop, test and build the project. In order to make it updates easier we ask you to +not edit files in this folder to extend configuration. + +## How to extend the basic configs? + +Bear in mind that you are doing it at your own risk, and that extending any of the basic configuration can lead +to issues around working with the project. + +### Extending the ESLint config + +Edit the `eslint.config.mjs` file in the project root to extend the ESLint configuration. The following example disables deprecation notices for source files. + +**Example:** + +```javascript +import { defineConfig } from 'eslint/config'; +import baseConfig from './.config/eslint.config.mjs'; + +export default defineConfig([ + { + ignores: [ + //... + ], + }, + ...baseConfig, + { + files: ['src/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-deprecated': 'off', + }, + }, +]); +``` + +--- + +### Extending the Prettier config + +Edit the `.prettierrc.js` file in the project root in order to extend the Prettier configuration. + +**Example:** + +```javascript +module.exports = { + // Prettier configuration provided by Grafana scaffolding + ...require('./.config/.prettierrc.js'), + + semi: false, +}; +``` + +--- + +### Extending the Jest config + +There are two configuration in the project root that belong to Jest: `jest-setup.js` and `jest.config.js`. + +**`jest-setup.js`:** A file that is run before each test file in the suite is executed. We are using it to +set up the Jest DOM for the testing library and to apply some polyfills. ([link to Jest docs](https://jestjs.io/docs/configuration#setupfilesafterenv-array)) + +**`jest.config.js`:** The main Jest configuration file that extends the Grafana recommended setup. ([link to Jest docs](https://jestjs.io/docs/configuration)) + +#### ESM errors with Jest + +A common issue with the current jest config involves importing an npm package that only offers an ESM build. These packages cause jest to error with `SyntaxError: Cannot use import statement outside a module`. To work around this, we provide a list of known packages to pass to the `[transformIgnorePatterns](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)` jest configuration property. If need be, this can be extended in the following way: + +```javascript +process.env.TZ = 'UTC'; +const { grafanaESModules, nodeModulesToTransform } = require('./config/jest/utils'); + +module.exports = { + // Jest configuration provided by Grafana + ...require('./.config/jest.config'), + // Inform jest to only transform specific node_module packages. + transformIgnorePatterns: [nodeModulesToTransform([...grafanaESModules, 'packageName'])], +}; +``` + +--- + +### Extending the TypeScript config + +Edit the `tsconfig.json` file in the project root in order to extend the TypeScript configuration. + +**Example:** + +```json +{ + "extends": "./.config/tsconfig.json", + "compilerOptions": { + "preserveConstEnums": true + } +} +``` + +--- + +### Extending the Webpack config + +Follow these steps to extend the basic Webpack configuration that lives under `.config/`: + +#### 1. Create a new Webpack configuration file + +Create a new config file that is going to extend the basic one provided by Grafana. +It can live in the project root, e.g. `webpack.config.ts`. + +#### 2. Merge the basic config provided by Grafana and your custom setup + +We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge) for this. + +```typescript +// webpack.config.ts +import type { Configuration } from 'webpack'; +import { merge } from 'webpack-merge'; +import grafanaConfig, { type Env } from './.config/webpack/webpack.config'; + +const config = async (env: Env): Promise => { + const baseConfig = await grafanaConfig(env); + + return merge(baseConfig, { + // Add custom config here... + output: { + asyncChunks: true, + }, + }); +}; + +export default config; +``` + +#### 3. Update the `package.json` to use the new Webpack config + +We need to update the `scripts` in the `package.json` to use the extended Webpack configuration. + +**Update for `build`:** + +```diff +-"build": "webpack -c ./.config/webpack/webpack.config.ts --env production", ++"build": "webpack -c ./webpack.config.ts --env production", +``` + +**Update for `dev`:** + +```diff +-"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development", ++"dev": "webpack -w -c ./webpack.config.ts --env development", +``` + +### Configure grafana image to use when running docker + +By default, `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behavior, simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`. + +**Example:** + +```yaml +version: '3.7' + +services: + grafana: + extends: + file: .config/docker-compose-base.yaml + service: grafana + build: + args: + grafana_version: ${GRAFANA_VERSION:-9.1.2} + grafana_image: ${GRAFANA_IMAGE:-grafana} +``` + +In this example, we assign the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will allow you to set the value while running the docker compose commands, which might be convenient in some scenarios. + +--- diff --git a/connectors/grafana-plugin/.config/bundler/constants.ts b/connectors/grafana-plugin/.config/bundler/constants.ts new file mode 100644 index 00000000..071e4fd3 --- /dev/null +++ b/connectors/grafana-plugin/.config/bundler/constants.ts @@ -0,0 +1,2 @@ +export const SOURCE_DIR = 'src'; +export const DIST_DIR = 'dist'; diff --git a/connectors/grafana-plugin/.config/bundler/copyFiles.ts b/connectors/grafana-plugin/.config/bundler/copyFiles.ts new file mode 100644 index 00000000..6a0703aa --- /dev/null +++ b/connectors/grafana-plugin/.config/bundler/copyFiles.ts @@ -0,0 +1,23 @@ +import { getPluginJson, hasReadme } from './utils.ts'; + +const pluginJson = getPluginJson(); +const logoPaths: string[] = Array.from(new Set([pluginJson.info?.logos?.large, pluginJson.info?.logos?.small])).filter( + Boolean +); +const screenshotPaths: string[] = pluginJson.info?.screenshots?.map((s: { path: string }) => s.path) || []; + +export const copyFilePatterns = [ + // If src/README.md exists use it; otherwise the root README + // To `compiler.options.output` + { from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true }, + { from: 'plugin.json', to: '.' }, + { from: '../LICENSE', to: '.' }, + { from: '../CHANGELOG.md', to: '.', force: true }, + { from: '**/*.json', to: '.' }, + { from: '**/query_help.md', to: '.', noErrorOnMissing: true }, + ...logoPaths.map((logoPath) => ({ from: logoPath, to: logoPath })), + ...screenshotPaths.map((screenshotPath) => ({ + from: screenshotPath, + to: screenshotPath, + })), +]; diff --git a/connectors/grafana-plugin/.config/bundler/externals.ts b/connectors/grafana-plugin/.config/bundler/externals.ts new file mode 100644 index 00000000..f5bf0382 --- /dev/null +++ b/connectors/grafana-plugin/.config/bundler/externals.ts @@ -0,0 +1,45 @@ +import type { Configuration, ExternalItemFunctionData } from 'webpack'; + +type ExternalsType = Configuration['externals']; + +export const externals: ExternalsType = [ + // Required for dynamic publicPath resolution + { 'amd-module': 'module' }, + 'lodash', + 'jquery', + 'moment', + 'slate', + 'emotion', + '@emotion/react', + '@emotion/css', + 'prismjs', + 'slate-plain-serializer', + '@grafana/slate-react', + 'react', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + 'react-dom', + 'react-redux', + 'redux', + 'rxjs', + 'i18next', + 'react-router', + 'd3', + 'angular', + /^@grafana\/ui/i, + /^@grafana\/runtime/i, + /^@grafana\/data/i, + + // Mark legacy SDK imports as external if their name starts with the "grafana/" prefix + ({ request }: ExternalItemFunctionData, callback: (error?: Error, result?: string) => void) => { + const prefix = 'grafana/'; + const hasPrefix = (request: string) => request.indexOf(prefix) === 0; + const stripPrefix = (request: string) => request.slice(prefix.length); + + if (request && hasPrefix(request)) { + return callback(undefined, stripPrefix(request)); + } + + callback(); + }, +]; diff --git a/connectors/grafana-plugin/.config/bundler/utils.ts b/connectors/grafana-plugin/.config/bundler/utils.ts new file mode 100644 index 00000000..3b13b193 --- /dev/null +++ b/connectors/grafana-plugin/.config/bundler/utils.ts @@ -0,0 +1,68 @@ +import fs from 'fs'; +import process from 'process'; +import os from 'os'; +import path from 'path'; +import { glob } from 'glob'; +import { SOURCE_DIR } from './constants.ts'; + +export function isWSL() { + if (process.platform !== 'linux') { + return false; + } + + if (os.release().toLowerCase().includes('microsoft')) { + return true; + } + + try { + return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); + } catch { + return false; + } +} + +function loadJson(path: string) { + const rawJson = fs.readFileSync(path, 'utf8'); + return JSON.parse(rawJson); +} + +export function getPackageJson() { + return loadJson(path.resolve(process.cwd(), 'package.json')); +} + +export function getPluginJson() { + return loadJson(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); +} + +export function getCPConfigVersion() { + const cprcJson = path.resolve(process.cwd(), './.config', '.cprc.json'); + return fs.existsSync(cprcJson) ? loadJson(cprcJson).version : { version: 'unknown' }; +} + +export function hasReadme() { + return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); +} + +// Support bundling nested plugins by finding all plugin.json files in src directory +// then checking for a sibling module.[jt]sx? file. +export async function getEntries() { + const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); + + const plugins = await Promise.all( + pluginsJson.map((pluginJson) => { + const folder = path.dirname(pluginJson); + return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); + }) + ); + + return plugins.reduce>((result, modules) => { + return modules.reduce((innerResult, module) => { + const pluginPath = path.dirname(module); + const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); + const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; + + innerResult[entryName] = module; + return innerResult; + }, result); + }, {}); +} diff --git a/connectors/grafana-plugin/.config/docker-compose-base.yaml b/connectors/grafana-plugin/.config/docker-compose-base.yaml new file mode 100644 index 00000000..164cd2cb --- /dev/null +++ b/connectors/grafana-plugin/.config/docker-compose-base.yaml @@ -0,0 +1,31 @@ +services: + grafana: + user: root + container_name: 'apache-iotdb-datasource' + + build: + context: . + args: + grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} + grafana_version: ${GRAFANA_VERSION:-13.1.0} + development: ${DEVELOPMENT:-false} + anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true} + ports: + - 3000:3000/tcp + - 2345:2345/tcp # delve + security_opt: + - 'apparmor:unconfined' + - 'seccomp:unconfined' + cap_add: + - SYS_PTRACE + volumes: + - ../dist:/var/lib/grafana/plugins/apache-iotdb-datasource + - ../provisioning:/etc/grafana/provisioning + - ..:/root/apache-iotdb-datasource + + environment: + NODE_ENV: development + GF_LOG_FILTERS: plugin.apache-iotdb-datasource:debug + GF_LOG_LEVEL: debug + GF_DATAPROXY_LOGGING: 1 + GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: apache-iotdb-datasource diff --git a/connectors/grafana-plugin/.config/entrypoint.sh b/connectors/grafana-plugin/.config/entrypoint.sh new file mode 100644 index 00000000..00c69f24 --- /dev/null +++ b/connectors/grafana-plugin/.config/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +if [ "${DEV}" = "false" ]; then + echo "Starting test mode" + exec /run.sh +fi + +echo "Starting development mode" + +if grep -i -q alpine /etc/issue; then + exec /usr/bin/supervisord -c /etc/supervisord.conf +elif grep -i -q ubuntu /etc/issue; then + exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf +else + echo 'ERROR: Unsupported base image' + exit 1 +fi + diff --git a/connectors/grafana-plugin/.config/eslint.config.mjs b/connectors/grafana-plugin/.config/eslint.config.mjs new file mode 100644 index 00000000..bafeaf58 --- /dev/null +++ b/connectors/grafana-plugin/.config/eslint.config.mjs @@ -0,0 +1,38 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-eslint-config + */ + +import { defineConfig } from 'eslint/config'; +import grafanaConfig from '@grafana/eslint-config/flat.js'; + +export default defineConfig([ + ...grafanaConfig, + { + rules: { + 'react/prop-types': 'off', + }, + }, + { + files: ['src/**/*.{ts,tsx}'], + + languageOptions: { + parserOptions: { + project: './tsconfig.json', + }, + }, + + rules: { + '@typescript-eslint/no-deprecated': 'warn', + }, + }, + { + files: ['./tests/**/*'], + + rules: { + 'react-hooks/rules-of-hooks': 'off', + }, + }, +]); diff --git a/connectors/grafana-plugin/.config/jest-setup.js b/connectors/grafana-plugin/.config/jest-setup.js new file mode 100644 index 00000000..7b1771ea --- /dev/null +++ b/connectors/grafana-plugin/.config/jest-setup.js @@ -0,0 +1,28 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config + */ + +import '@testing-library/jest-dom'; +import { TextEncoder, TextDecoder } from 'util'; + +Object.assign(global, { TextDecoder, TextEncoder }); + +// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom +Object.defineProperty(global, 'matchMedia', { + writable: true, + value: (query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + }), +}); + +HTMLCanvasElement.prototype.getContext = () => {}; diff --git a/connectors/grafana-plugin/.config/jest.config.js b/connectors/grafana-plugin/.config/jest.config.js new file mode 100644 index 00000000..efe19384 --- /dev/null +++ b/connectors/grafana-plugin/.config/jest.config.js @@ -0,0 +1,44 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config + */ + +const path = require('path'); +const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils'); + +module.exports = { + moduleNameMapper: { + '\\.(css|scss|sass)$': 'identity-obj-proxy', + 'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'), + }, + modulePaths: ['/src'], + setupFilesAfterEnv: ['/jest-setup.js'], + testEnvironment: 'jest-environment-jsdom', + testMatch: [ + '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', + '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', + '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', + ], + transform: { + '^.+\\.(t|j)sx?$': [ + '@swc/jest', + { + sourceMaps: 'inline', + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: false, + dynamicImport: true, + }, + }, + }, + ], + }, + // Jest will throw `Cannot use import statement outside module` if it tries to load an + // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest + transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)], + watchPathIgnorePatterns: ['/node_modules', '/dist'], +}; diff --git a/connectors/grafana-plugin/.config/jest/mocks/react-inlinesvg.tsx b/connectors/grafana-plugin/.config/jest/mocks/react-inlinesvg.tsx new file mode 100644 index 00000000..d540f3aa --- /dev/null +++ b/connectors/grafana-plugin/.config/jest/mocks/react-inlinesvg.tsx @@ -0,0 +1,25 @@ +// Due to the grafana/ui Icon component making fetch requests to +// `/public/img/icon/.svg` we need to mock react-inlinesvg to prevent +// the failed fetch requests from displaying errors in console. + +import React from 'react'; + +type Callback = (...args: any[]) => void; + +export interface StorageItem { + content: string; + queue: Callback[]; + status: string; +} + +export const cacheStore: { [key: string]: StorageItem } = Object.create(null); + +const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/; + +const InlineSVG = ({ src }: { src: string }) => { + // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`) + const testId = src.replace(SVG_FILE_NAME_REGEX, '$2'); + return ; +}; + +export default InlineSVG; diff --git a/connectors/grafana-plugin/.config/jest/utils.js b/connectors/grafana-plugin/.config/jest/utils.js new file mode 100644 index 00000000..55d9cb6e --- /dev/null +++ b/connectors/grafana-plugin/.config/jest/utils.js @@ -0,0 +1,37 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in .config/README.md + */ + +/* + * This utility function is useful in combination with jest `transformIgnorePatterns` config + * to transform specific packages (e.g.ES modules) in a projects node_modules folder. + */ +const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`; + +// Array of known nested grafana package dependencies that only bundle an ESM version +const grafanaESModules = [ + '.pnpm', // Support using pnpm symlinked packages + '@grafana/schema', + '@wojtekmaj/date-utils', + 'd3', + 'd3-color', + 'd3-force', + 'd3-interpolate', + 'd3-scale-chromatic', + 'get-user-locale', + 'marked', + 'memoize', + 'mimic-function', + 'ol', + 'react-calendar', + 'react-colorful', + 'rxjs', + 'uuid', +]; + +module.exports = { + nodeModulesToTransform, + grafanaESModules, +}; diff --git a/connectors/grafana-plugin/.config/supervisord/supervisord.conf b/connectors/grafana-plugin/.config/supervisord/supervisord.conf new file mode 100644 index 00000000..738e3502 --- /dev/null +++ b/connectors/grafana-plugin/.config/supervisord/supervisord.conf @@ -0,0 +1,47 @@ +[supervisord] +nodaemon=true +user=root + +[program:grafana] +user=root +directory=/var/lib/grafana +command=bash -c 'while [ ! -f /root/apache-iotdb-datasource/dist/gpx_iotdb* ]; do sleep 1; done; /run.sh' +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +killasgroup=true +stopasgroup=true +autostart=true + +[program:delve] +user=root +command=/bin/bash -c 'pid=""; while [ -z "$pid" ]; do pid=$(pgrep -f gpx_iotdb); done; /root/go/bin/dlv attach --api-version=2 --headless --continue --accept-multiclient --listen=:2345 $pid' +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +killasgroup=false +stopasgroup=false +autostart=true +autorestart=true + +[program:build-watcher] +user=root +command=/bin/bash -c 'while inotifywait -e modify,create,delete -r /var/lib/grafana/plugins/apache-iotdb-datasource; do echo "Change detected, restarting delve...";supervisorctl restart delve; done' +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +killasgroup=true +stopasgroup=true +autostart=true + +[program:mage-watcher] +user=root +environment=PATH="/usr/local/go/bin:/root/go/bin:%(ENV_PATH)s" +directory=/root/apache-iotdb-datasource +command=/bin/bash -c 'git config --global --add safe.directory /root/apache-iotdb-datasource && mage -v watch' +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +killasgroup=true +stopasgroup=true +autostart=true diff --git a/connectors/grafana-plugin/.config/tsconfig.json b/connectors/grafana-plugin/.config/tsconfig.json new file mode 100644 index 00000000..385a2234 --- /dev/null +++ b/connectors/grafana-plugin/.config/tsconfig.json @@ -0,0 +1,29 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-typescript-config + */ +{ + "compilerOptions": { + "alwaysStrict": true, + "declaration": false, + "rootDir": "../src", + "paths": { + "*": ["../src/*"] + }, + "typeRoots": ["../node_modules/@types"], + "resolveJsonModule": true + }, + "ts-node": { + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "es2022", + "esModuleInterop": true + }, + "transpileOnly": true + }, + "include": ["../src", "./types"], + "extends": "@grafana/tsconfig" +} diff --git a/connectors/grafana-plugin/.config/types/bundler-rules.d.ts b/connectors/grafana-plugin/.config/types/bundler-rules.d.ts new file mode 100644 index 00000000..e67197c5 --- /dev/null +++ b/connectors/grafana-plugin/.config/types/bundler-rules.d.ts @@ -0,0 +1,37 @@ +// Image declarations +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.webp' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} + +// Font declarations +declare module '*.woff'; +declare module '*.woff2'; +declare module '*.eot'; +declare module '*.ttf'; +declare module '*.otf'; diff --git a/connectors/grafana-plugin/.config/types/setupTests.d.ts b/connectors/grafana-plugin/.config/types/setupTests.d.ts new file mode 100644 index 00000000..7b0828bf --- /dev/null +++ b/connectors/grafana-plugin/.config/types/setupTests.d.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/connectors/grafana-plugin/.config/types/webpack-plugins.d.ts b/connectors/grafana-plugin/.config/types/webpack-plugins.d.ts new file mode 100644 index 00000000..6dbab109 --- /dev/null +++ b/connectors/grafana-plugin/.config/types/webpack-plugins.d.ts @@ -0,0 +1,83 @@ +declare module 'replace-in-file-webpack-plugin' { + import { Compiler, Plugin } from 'webpack'; + + interface ReplaceRule { + search: string | RegExp; + replace: string | ((match: string) => string); + } + + interface ReplaceOption { + dir?: string; + files?: string[]; + test?: RegExp | RegExp[]; + rules: ReplaceRule[]; + } + + class ReplaceInFilePlugin extends Plugin { + constructor(options?: ReplaceOption[]); + options: ReplaceOption[]; + apply(compiler: Compiler): void; + } + + export = ReplaceInFilePlugin; +} + +declare module 'webpack-livereload-plugin' { + import { ServerOptions } from 'https'; + import { Compiler, Plugin, Stats, Compilation } from 'webpack'; + + interface Options extends Pick { + /** + * protocol for livereload `