├── .bra.toml ├── .changeset ├── README.md ├── changelog.js ├── config.json ├── dirty-peaches-dress.md ├── four-drinks-argue.md └── tricky-parrots-marry.md ├── .config ├── .cprc.json ├── .eslintrc ├── .prettierrc.js ├── Dockerfile ├── README.md ├── entrypoint.sh ├── jest-setup.js ├── jest.config.js ├── jest │ ├── mocks │ │ └── react-inlinesvg.tsx │ └── utils.js ├── supervisord │ └── supervisord.conf ├── tsconfig.json ├── types │ └── custom.d.ts └── webpack │ ├── BuildModeWebpackPlugin.ts │ ├── constants.ts │ ├── utils.ts │ └── webpack.config.ts ├── .editorconfig ├── .eslintrc ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── add-to-project.yml │ ├── publish.yaml │ ├── push.yaml │ └── update-make-docs.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.js ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Magefile.go ├── README.md ├── cspell.config.json ├── docker-compose.yaml ├── docs ├── Makefile ├── developer-guide.md ├── docs.mk ├── make-docs ├── screenshots │ ├── annotations-editor.png │ ├── annotations.png │ ├── local-plugin-install.png │ ├── using-variables.png │ └── variables-create.png ├── sources │ ├── _index.md │ ├── annotations │ │ └── _index.md │ ├── sample-dashboards │ │ └── _index.md │ ├── setup │ │ ├── _index.md │ │ ├── datasource.md │ │ ├── installation.md │ │ ├── provisioning.md │ │ └── token.md │ └── variables-and-macros │ │ ├── _index.md │ │ ├── macros.md │ │ └── variables.md └── variables.mk ├── go.mod ├── go.sum ├── jest-setup.js ├── jest.config.js ├── package.json ├── pkg ├── dfutil │ └── framer.go ├── errors │ ├── docs.go │ └── errors.go ├── github │ ├── client │ │ ├── client.go │ │ ├── errorsourcehandling.go │ │ └── errorsourcehandling_test.go │ ├── codescanning.go │ ├── codescanning_handler.go │ ├── codescanning_test.go │ ├── commits.go │ ├── commits_handler.go │ ├── commits_test.go │ ├── constants.go │ ├── contributors.go │ ├── contributors_handler.go │ ├── contributors_test.go │ ├── datasource.go │ ├── docs.go │ ├── issues.go │ ├── issues_handler.go │ ├── issues_test.go │ ├── labels.go │ ├── labels_handler.go │ ├── labels_test.go │ ├── macros.go │ ├── macros_test.go │ ├── milestones.go │ ├── milestones_handler.go │ ├── milestones_test.go │ ├── organizations.go │ ├── organizations_test.go │ ├── packages.go │ ├── packages_handler.go │ ├── packages_test.go │ ├── projects │ │ ├── project_items.go │ │ ├── project_items_filter.go │ │ ├── project_items_filter_test.go │ │ ├── project_items_models.go │ │ ├── project_items_test.go │ │ ├── projects.go │ │ ├── projects_test.go │ │ └── testdata │ │ │ ├── project.golden.jsonc │ │ │ └── projects.golden.jsonc │ ├── projects_handler.go │ ├── pull_requests.go │ ├── pull_requests_handler.go │ ├── pull_requests_test.go │ ├── query_handler.go │ ├── releases.go │ ├── releases_handler.go │ ├── releases_test.go │ ├── repositories.go │ ├── repositories_handler.go │ ├── repositories_test.go │ ├── resource_handlers.go │ ├── stargazers.go │ ├── stargazers_handler.go │ ├── stargazers_test.go │ ├── tags.go │ ├── tags_handler.go │ ├── tags_test.go │ ├── testdata │ │ ├── commits.golden.jsonc │ │ ├── contributors.golden.jsonc │ │ ├── issues.golden.jsonc │ │ ├── milestones.golden.jsonc │ │ ├── pull_requests.golden.jsonc │ │ ├── releases.golden.jsonc │ │ ├── repositories.golden.jsonc │ │ ├── stargazers.golden.jsonc │ │ ├── tags.golden.jsonc │ │ ├── workflowRuns.golden.jsonc │ │ ├── workflowUsage.golden.jsonc │ │ └── workflows.golden.jsonc │ ├── vulnerabilites_handler.go │ ├── vulnerabilities.go │ ├── workflows.go │ ├── workflows_handler.go │ └── workflows_test.go ├── httputil │ └── errors.go ├── main.go ├── models │ ├── client.go │ ├── codescanning.go │ ├── commits.go │ ├── contributors.go │ ├── docs.go │ ├── issues.go │ ├── labels.go │ ├── milestones.go │ ├── packages.go │ ├── packages_test.go │ ├── pagination.go │ ├── projects.go │ ├── pull_requests.go │ ├── query.go │ ├── releases.go │ ├── repositories.go │ ├── settings.go │ ├── stargazers.go │ ├── tags.go │ ├── vulnerabilities.go │ └── workflows.go ├── plugin │ ├── datasource.go │ ├── datasource_caching.go │ ├── datasource_caching_test.go │ └── instance.go └── testutil │ ├── client.go │ ├── frames.go │ ├── maputils.go │ └── typeutils.go ├── playwright.config.ts ├── scripts ├── debug-backend.sh ├── e2e.sh ├── restart-plugin.sh └── test.sh ├── src ├── DataSource.test.ts ├── DataSource.ts ├── components │ ├── Divider.tsx │ ├── FieldSelect.tsx │ ├── Filters.tsx │ ├── Forms.tsx │ └── selectors.ts ├── constants.ts ├── dashboards │ └── dashboard.json ├── img │ └── github.svg ├── migrations.ts ├── module.ts ├── plugin.json ├── tracking.ts ├── types │ ├── config.ts │ └── query.ts ├── validation.ts ├── variables.test.ts ├── variables.ts └── views │ ├── ConfigEditor.spec.tsx │ ├── ConfigEditor.tsx │ ├── QueryEditor.tsx │ ├── QueryEditorCodeScanning.tsx │ ├── QueryEditorCommits.tsx │ ├── QueryEditorContributors.tsx │ ├── QueryEditorIssues.test.tsx │ ├── QueryEditorIssues.tsx │ ├── QueryEditorLabels.tsx │ ├── QueryEditorMilestones.tsx │ ├── QueryEditorPackages.test.tsx │ ├── QueryEditorPackages.tsx │ ├── QueryEditorProjects.tsx │ ├── QueryEditorPullRequests.tsx │ ├── QueryEditorReleases.tsx │ ├── QueryEditorRepository.tsx │ ├── QueryEditorTags.tsx │ ├── QueryEditorVulnerabilities.tsx │ ├── QueryEditorWorkflowRuns.tsx │ ├── QueryEditorWorkflowUsage.tsx │ ├── QueryEditorWorkflows.tsx │ └── VariableQueryEditor.tsx ├── tests ├── ConfigEditor.spec.ts ├── QueryEditor.spec.ts └── mocks │ └── github-response.ts ├── tsconfig.json └── yarn.lock /.bra.toml: -------------------------------------------------------------------------------- 1 | # default configuration created by the `mage watch` command. 2 | # this file can be edited and should be checked into source control. 3 | # see https://github.com/unknwon/bra/blob/master/templates/default.bra.toml for more configuration options. 4 | [run] 5 | init_cmds = [ 6 | ["mage", "-v", "build:debug"], 7 | ["mage", "-v" , "reloadPlugin"] 8 | ] 9 | watch_all = true 10 | follow_symlinks = false 11 | ignore = [".git", "node_modules", "dist"] 12 | ignore_files = ["mage_output_file.go"] 13 | watch_dirs = [ 14 | "pkg", 15 | "src", 16 | ] 17 | watch_exts = [".go", ".json"] 18 | build_delay = 2000 19 | cmds = [ 20 | ["mage", "-v", "build:debug"], 21 | ["mage", "-v" , "reloadPlugin"] 22 | ] 23 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/changelog.js: -------------------------------------------------------------------------------- 1 | const changelogFunctions = { 2 | getReleaseLine: async (changeset, type, options) => { 3 | let prefix = '🎉'; 4 | if (type === 'major') { 5 | prefix = '🎉'; 6 | } else if (type === 'minor') { 7 | prefix = '🚀'; 8 | } else if (type === 'patch') { 9 | prefix = '🐛'; 10 | } 11 | if (changeset && changeset.summary) { 12 | const summary = changeset.summary || ''; 13 | if (summary.indexOf('Docs') > -1) { 14 | prefix = '📝'; 15 | } 16 | if ( 17 | summary.indexOf('Chore') > -1 || 18 | summary.indexOf('grafana-plugin-sdk-go') > -1 || 19 | summary.indexOf('compiled') > -1 20 | ) { 21 | prefix = '⚙️'; 22 | } 23 | return [prefix, summary].join(' '); 24 | } 25 | return [prefix, changeset?.summary].join(' '); 26 | }, 27 | getDependencyReleaseLine: async (changesets, dependenciesUpdated, options) => { 28 | return '\n'; 29 | }, 30 | }; 31 | 32 | module.exports = changelogFunctions; 33 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "./changelog.js", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/dirty-peaches-dress.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'grafana-github-datasource': patch 3 | --- 4 | 5 | Documentation links will open in a new tab 6 | -------------------------------------------------------------------------------- /.changeset/four-drinks-argue.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'grafana-github-datasource': patch 3 | --- 4 | 5 | Removed unused annotations method (replaced with new annotations support in [#196](https://github.com/grafana/github-datasource/pull/196)) 6 | -------------------------------------------------------------------------------- /.changeset/tricky-parrots-marry.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'grafana-github-datasource': patch 3 | --- 4 | 5 | Replaced the deprecated `setVariableQueryEditor` with `CustomVariableSupport` 6 | -------------------------------------------------------------------------------- /.config/.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5.12.4" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | "plugins": ["deprecation"], 16 | "files": ["src/**/*.{ts,tsx}"], 17 | "rules": { 18 | "deprecation/deprecation": "warn" 19 | }, 20 | "parserOptions": { 21 | "project": "./tsconfig.json" 22 | } 23 | }, 24 | { 25 | "files": ["./tests/**/*"], 26 | "rules": { 27 | "react-hooks/rules-of-hooks": "off", 28 | }, 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.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/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/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 | -------------------------------------------------------------------------------- /.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/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/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/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/grafana-github-datasource/dist/gpx_github* ]; 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_github); 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/grafana-github-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/grafana-github-datasource 41 | command=/bin/bash -c 'git config --global --add safe.directory /root/grafana-github-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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/webpack/BuildModeWebpackPlugin.ts: -------------------------------------------------------------------------------- 1 | import * as webpack 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 | -------------------------------------------------------------------------------- /.config/webpack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_DIR = 'src'; 2 | export const DIST_DIR = 'dist'; 3 | -------------------------------------------------------------------------------- /.config/webpack/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import process from 'process'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import { glob } from 'glob'; 6 | import { SOURCE_DIR } from './constants'; 7 | 8 | export function isWSL() { 9 | if (process.platform !== 'linux') { 10 | return false; 11 | } 12 | 13 | if (os.release().toLowerCase().includes('microsoft')) { 14 | return true; 15 | } 16 | 17 | try { 18 | return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); 19 | } catch { 20 | return false; 21 | } 22 | } 23 | 24 | export function getPackageJson() { 25 | return require(path.resolve(process.cwd(), 'package.json')); 26 | } 27 | 28 | export function getPluginJson() { 29 | return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); 30 | } 31 | 32 | export function getCPConfigVersion() { 33 | const cprcJson = path.resolve(__dirname, '../', '.cprc.json'); 34 | return fs.existsSync(cprcJson) ? require(cprcJson).version : { version: 'unknown' }; 35 | } 36 | 37 | export function hasReadme() { 38 | return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); 39 | } 40 | 41 | // Support bundling nested plugins by finding all plugin.json files in src directory 42 | // then checking for a sibling module.[jt]sx? file. 43 | export async function getEntries(): Promise> { 44 | const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); 45 | 46 | const plugins = await Promise.all( 47 | pluginsJson.map((pluginJson) => { 48 | const folder = path.dirname(pluginJson); 49 | return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); 50 | }) 51 | ); 52 | 53 | return plugins.reduce((result, modules) => { 54 | return modules.reduce((result, module) => { 55 | const pluginPath = path.dirname(module); 56 | const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); 57 | const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; 58 | 59 | result[entryName] = module; 60 | return result; 61 | }, result); 62 | }, {}); 63 | } 64 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 120 11 | 12 | [*.go] 13 | indent_style = tab 14 | indent_size = 4 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.{js,ts,tsx,scss}] 20 | quote_type = single 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/.eslintrc" 3 | } 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | # More details are here: https://help.github.com/articles/about-codeowners/ 4 | # The '*' pattern is global owners. 5 | 6 | * @grafana/oss-big-tent 7 | 8 | docs/docs.mk @jdbaldry 9 | docs/make-docs @jdbaldry 10 | docs/Makefile @jdbaldry 11 | docs/variables.mk @jdbaldry 12 | 13 | .github/workflows/update-make-docs.yml @jdbaldry -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | allow: 8 | # Keep the sdk modules up-to-date 9 | - dependency-name: "github.com/grafana/grafana-plugin-sdk-go" 10 | dependency-type: "all" 11 | commit-message: 12 | prefix: "Upgrade grafana-plugin-sdk-go " 13 | include: "scope" 14 | reviewers: 15 | - "grafana/oss-big-tent" -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to OSS Big Tent team project 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | pull_request: 7 | types: 8 | - opened 9 | 10 | permissions: 11 | contents: read 12 | id-token: write 13 | 14 | jobs: 15 | add-to-project: 16 | name: Add issue to project 17 | runs-on: ubuntu-latest 18 | steps: 19 | - id: get-secrets 20 | uses: grafana/shared-workflows/actions/get-vault-secrets@main # zizmor: ignore[unpinned-uses] 21 | with: 22 | repo_secrets: | 23 | GITHUB_APP_ID=grafana-oss-big-tent:app-id 24 | GITHUB_APP_PRIVATE_KEY=grafana-oss-big-tent:private-key 25 | - name: Generate a token 26 | id: generate-token 27 | uses: actions/create-github-app-token@v1 28 | with: 29 | app-id: ${{ env.GITHUB_APP_ID }} 30 | private-key: ${{ env.GITHUB_APP_PRIVATE_KEY }} 31 | owner: ${{ github.repository_owner }} 32 | - uses: actions/add-to-project@main 33 | with: 34 | project-url: https://github.com/orgs/grafana/projects/457 35 | github-token: ${{ steps.generate-token.outputs.token }} 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Plugins - CD 2 | run-name: Deploy ${{ inputs.branch }} to ${{ inputs.environment }} by @${{ github.actor }} 3 | permissions: 4 | contents: read 5 | id-token: write 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | branch: 11 | description: Branch to publish from. Can be used to deploy PRs to dev 12 | default: main 13 | environment: 14 | description: Environment to publish to 15 | required: true 16 | type: choice 17 | options: 18 | - 'dev' 19 | - 'ops' 20 | - 'prod' 21 | docs-only: 22 | description: Only publish docs, do not publish the plugin 23 | default: false 24 | type: boolean 25 | 26 | jobs: 27 | cd: 28 | name: CD 29 | uses: grafana/plugin-ci-workflows/.github/workflows/cd.yml@main 30 | with: 31 | golangci-lint-version: '1.64.6' 32 | branch: ${{ github.event.inputs.branch }} 33 | environment: ${{ github.event.inputs.environment }} 34 | docs-only: ${{ fromJSON(github.event.inputs.docs-only) }} 35 | run-playwright: true 36 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: Plugins - CI 2 | permissions: 3 | contents: read 4 | id-token: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | ci: 16 | name: CI 17 | uses: grafana/plugin-ci-workflows/.github/workflows/ci.yml@main 18 | with: 19 | golangci-lint-version: '1.64.6' 20 | plugin-version-suffix: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} 21 | run-playwright: true 22 | -------------------------------------------------------------------------------- /.github/workflows/update-make-docs.yml: -------------------------------------------------------------------------------- 1 | name: Update `make docs` procedure 2 | permissions: 3 | contents: write 4 | pull-requests: write 5 | 6 | on: 7 | schedule: 8 | - cron: '0 7 * * 1-5' 9 | workflow_dispatch: 10 | jobs: 11 | main: 12 | if: github.repository == 'grafana/github-datasource' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | - uses: grafana/writers-toolkit/update-make-docs@update-make-docs/v1 # zizmor: ignore[unpinned-uses] 19 | with: 20 | pr_options: > 21 | --label type/docs 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | ci/ 4 | dist/ 5 | .idea/ 6 | 7 | # e2e test directories 8 | /test-results/ 9 | /playwright-report/ 10 | /blob-report/ 11 | /playwright/.cache/ 12 | /playwright/.auth/ 13 | 14 | .eslintcache 15 | __debug_bin 16 | mage_output_file.go 17 | 18 | # provisioning 19 | /provisioning/ 20 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Prettier configuration provided by Grafana scaffolding 3 | ...require('./.config/.prettierrc.js'), 4 | }; 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run standalone plugin", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/pkg/", 13 | "env": {}, 14 | "args": ["--standalone=true"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to this repository. We are glad you want help us to improve the project and join our community. Feel free to [browse the open issues](https://github.com/grafana/github-datasource/issues). If you wanna more straightforward tasks to complete, [we have some](https://github.com/grafana/github-datasource/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). For more details about how you can help, please take a look at [Grafana's Contributing Guide](https://github.com/grafana/grafana/blob/main/CONTRIBUTING.md). 4 | 5 | ## Next steps 6 | 7 | - [Set up your development environment](./docs/developer-guide.md) 8 | -------------------------------------------------------------------------------- /Magefile.go: -------------------------------------------------------------------------------- 1 | //+build mage 2 | 3 | package main 4 | 5 | import ( 6 | // mage:import 7 | build "github.com/grafana/grafana-plugin-sdk-go/build" 8 | ) 9 | 10 | // Default configures the default target. 11 | var Default = build.BuildAll 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Grafana GitHub data source 4 | 5 | The GitHub data source plugin for Grafana lets you to query the GitHub API in Grafana so you can visualize your GitHub repositories and projects. 6 | 7 | ## Documentation 8 | 9 | For the plugin documentation, visit plugin documentation website 10 | 11 | ## Video Tutorial 12 | 13 | Watch this video to learn more about setting up the Grafana GitHub data source plugin: 14 | 15 | [![GitHub data source plugin | Visualize GitHub using Grafana | Tutorial](https://img.youtube.com/vi/DW693S3cO48/hq720.jpg)](https://youtu.be/DW693S3cO48 "Grafana GitHub data source plugin") 16 | 17 | > [!TIP] 18 | > 19 | > ## Give it a try using Grafana Play 20 | > 21 | > With Grafana Play, you can explore and see how it works, learning from practical examples to accelerate your development. This feature can be seen on [GitHub data source plugin demo](https://play.grafana.org/d/d5b56357-1a57-4821-ab27-16fdf79cab57/github3a-queries-and-multi-variables). 22 | 23 | ## GitHub API V4 (GraphQL) 24 | 25 | This data source uses the [`githubv4` package](https://github.com/shurcooL/githubv4), which is under active development. 26 | 27 | ## Frequently Asked Questions 28 | 29 | - **Why does it sometimes take up to 5 minutes for my new pull request / new issue / new commit to show up?** 30 | 31 | We have aggressive caching enabled due to GitHub's rate limiting policies. When selecting a time range like "Last hour", a combination of the queries for each panel and the time range is cached temporarily. 32 | 33 | - **Why are there two selection options for Pull Requests and Issue times when creating annotations?** 34 | 35 | There are two times that affect an annotation: 36 | 37 | - The time range of the dashboard or panel 38 | - The time that should be used to display the event on the graph 39 | 40 | The first selection is used to filter the events that display on the graph. For example, if you select "closed at", only events that were "closed" in your dashboard's time range will be displayed on the graph. 41 | 42 | The second selection is used to determine where on the graph the event should be displayed. 43 | 44 | Typically, these will be the same, however there are some cases where you may want them to be different. 45 | -------------------------------------------------------------------------------- /cspell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": ["node_modules/**", "dist/**", "mage_output_file.go", "package.json", "yarn.lock"], 3 | "ignoreRegExpList": ["import\\s*\\((.|[\r\n])*?\\)", "import\\s*.*\".*?\""], 4 | "words": [ 5 | "araddon", 6 | "bmike", 7 | "CTAV", 8 | "Dataframe", 9 | "DATAPROXY", 10 | "datasource", 11 | "datasources", 12 | "dateparse", 13 | "dfutil", 14 | "dompurify", 15 | "dserrors", 16 | "errorsource", 17 | "ghinstallation", 18 | "githubclient", 19 | "githubv", 20 | "googlegithub", 21 | "grafana", 22 | "grafanabot", 23 | "groupby", 24 | "healthcheck", 25 | "httpclient", 26 | "HTMLURL", 27 | "instancemgmt", 28 | "jackspeak", 29 | "kminehart", 30 | "mergeable", 31 | "Mergeable", 32 | "mjseaman", 33 | "nazzzzz", 34 | "prismjs", 35 | "promop", 36 | "PTRACE", 37 | "PYPI", 38 | "Quantile", 39 | "querystring", 40 | "querytype", 41 | "rgba", 42 | "RUBYGEMS", 43 | "seccomp", 44 | "shurcoo", 45 | "stretchr", 46 | "structs", 47 | "subresource", 48 | "tdigest", 49 | "templating", 50 | "testdata", 51 | "TESTDATA", 52 | "testid", 53 | "testutil", 54 | "textbox", 55 | "timepicker", 56 | "timeseries", 57 | "typecheck", 58 | "uplot", 59 | "vals", 60 | "vladimirdotk", 61 | "Wrapf", 62 | "confg" //this is unfortunately a typo in a file name that is not easy to fix 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | grafana: 3 | user: root 4 | container_name: 'grafana-github-datasource' 5 | 6 | build: 7 | context: ./.config 8 | args: 9 | grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} 10 | grafana_version: ${GRAFANA_VERSION:-main} 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/grafana-github-datasource 23 | - ./provisioning:/etc/grafana/provisioning 24 | - .:/root/grafana-github-datasource 25 | 26 | environment: 27 | NODE_ENV: development 28 | GF_LOG_FILTERS: plugin.grafana-github-datasource:debug 29 | GF_LOG_LEVEL: debug 30 | GF_DATAPROXY_LOGGING: 1 31 | GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-github-datasource 32 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | .DELETE_ON_ERROR: 3 | export SHELL := bash 4 | export SHELLOPTS := pipefail:errexit 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rule 7 | 8 | include docs.mk 9 | -------------------------------------------------------------------------------- /docs/screenshots/annotations-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/github-datasource/ceb91eec1971cbd5eca7a40923e5cacc3a96c42c/docs/screenshots/annotations-editor.png -------------------------------------------------------------------------------- /docs/screenshots/annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/github-datasource/ceb91eec1971cbd5eca7a40923e5cacc3a96c42c/docs/screenshots/annotations.png -------------------------------------------------------------------------------- /docs/screenshots/local-plugin-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/github-datasource/ceb91eec1971cbd5eca7a40923e5cacc3a96c42c/docs/screenshots/local-plugin-install.png -------------------------------------------------------------------------------- /docs/screenshots/using-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/github-datasource/ceb91eec1971cbd5eca7a40923e5cacc3a96c42c/docs/screenshots/using-variables.png -------------------------------------------------------------------------------- /docs/screenshots/variables-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/github-datasource/ceb91eec1971cbd5eca7a40923e5cacc3a96c42c/docs/screenshots/variables-create.png -------------------------------------------------------------------------------- /docs/sources/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GitHub data source plugin for Grafana 3 | menuTitle: GitHub data source 4 | description: The GitHub data source lets you visualize GitHub data in Grafana dashboards. 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 10 16 | --- 17 | 18 | # GitHub data source plugin for Grafana 19 | 20 | The GitHub data source plugin for Grafana lets you to query the GitHub API in Grafana so you can visualize your GitHub repositories and projects. 21 | 22 | Watch this video to learn more about setting up the Grafana GitHub data source plugin: {{< youtube id="DW693S3cO48">}} 23 | 24 | {{< docs/play title="GitHub data source plugin demo" url="https://play.grafana.org/d/cdgx261sa1ypsa/3-single-repo-with-override-examples" >}} 25 | 26 | ## Query types 27 | 28 | The plugin supports the following query types: 29 | 30 | - Code Scan 31 | - Commits 32 | - Issues 33 | - Contributors 34 | - Tags 35 | - Releases 36 | - Pull requests 37 | - Labels 38 | - Repositories 39 | - Milestones 40 | - Packages 41 | - Vulnerabilities 42 | - Projects 43 | - Stargazers 44 | - Workflows 45 | - Workflow usage 46 | - Workflow runs 47 | 48 | ## Supported features 49 | 50 | With the plugin you can: 51 | 52 | - Visualize queries 53 | - Use template variables 54 | - Configure Annotations 55 | - Cache queries 56 | 57 | ## Caching 58 | 59 | Caching on this plugin is always enabled. 60 | 61 | {{< admonition type="note" >}} 62 | To work around [GitHub's rate limiting](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28), the plugin caches requests aggressively. 63 | 64 | This can mean that it takes up to five minutes for a new pull request, commit, or issue to show up in a query. 65 | {{< /admonition >}} 66 | 67 | ## Requirements 68 | 69 | To use the GitHub data source plugin, you will need: 70 | 71 | - A free [GitHub](https://github.com/) or a [GitHub Enterprise](https://github.com/enterprise) account. 72 | - Any of the following Grafana editions: 73 | - Grafana OSS server. 74 | - A [Grafana Cloud](https://grafana.com/pricing/) stack. 75 | - An on-premise Grafana Enterprise server with an [activated license](https://grafana.com/docs/grafana/latest/enterprise/license/activate-license/). 76 | 77 | ## Get started 78 | 79 | - To start using the plugin, you need to generate an access token, then install and configure the plugin. To do this, refer to [Setup](./setup). 80 | - To use variable and macros, for creating a dynamic dashboard, refer to [Variables and Macros](./variables-and-macros). 81 | - To annotate the data by displaying the GitHub resources on the dashboard, refer to [Annotations](./annotations/). 82 | - To quickly visualize GitHub data in Grafana, refer to [Sample dashboards](./sample-dashboards/). 83 | 84 | ## Get the most out of the plugin 85 | 86 | - Add [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/) 87 | - Configure and use [Templates and variables](https://grafana.com/docs/grafana/latest/variables/) 88 | - Add [Transformations](https://grafana.com/docs/grafana/latest/panels/transformations/) 89 | 90 | ## Report issues 91 | 92 | Use the [official GitHub repository](https://github.com/grafana/github-datasource/issues) to report issues, bugs, and feature requests. 93 | -------------------------------------------------------------------------------- /docs/sources/annotations/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create annotations with GitHub data source plugin for Grafana 3 | menuTitle: Annotations 4 | description: Learn about annotations for the GitHub data source plugin for Grafana 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 400 16 | --- 17 | 18 | # Create annotations with GitHub data source plugin for Grafana 19 | 20 | [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations) let you extract data from a data source and use it to annotate a dashboard. 21 | 22 | To create annotations, you need to specify at least the following two fields: 23 | 24 | - A String field for the annotation text 25 | - A Time field for the annotation time 26 | 27 | Annotations overlay events on a graph. 28 | 29 | {{< figure alt="Annotations on a graph" src="/media/docs/grafana/data-sources/github/annotations.png" >}} 30 | 31 | With annotations, you can display the following GitHub resources on a graph: 32 | 33 | - Commits 34 | - Issues 35 | - Pull requests 36 | - Releases 37 | - Tags 38 | 39 | All annotations require that you select a field to display on the annotation, and a field that represents the time that the event occurred. 40 | 41 | {{< figure alt="Annotations editor" src="/media/docs/grafana/data-sources/github/annotations-editor.png" >}} 42 | 43 | If you want to add titles or tags to the annotations, you can add additional fields with the appropriate types. 44 | 45 | For more information on how to configure a query, refer to [Built-in query editor](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/#built-in-query). 46 | 47 | ## Pull Requests and Issue times when creating annotations 48 | 49 | While using annotations for pull request and issues, there two selection options. This is because as there are two times that affect an annotation: 50 | 51 | - The time range of the dashboard or panel 52 | - The time that should be used to display the event on the graph 53 | 54 | The first selection is used to filter the events that display on the graph. 55 | 56 | For example, if you select "closed at", only events that were "closed" in your dashboard's time range will be displayed on the graph. 57 | 58 | The second selection is used to determine where on the graph the event should be displayed. 59 | 60 | Typically these will be the same, however there are some cases where you may want them to be different. 61 | -------------------------------------------------------------------------------- /docs/sources/sample-dashboards/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create sample dashboards for the Grafana GitHub data source plugin for Grafana 3 | menuTitle: Create sample dashboards 4 | description: Learn how to import example dashboards into Grafana for use with the GitHub data source plugin for Grafana 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 500 16 | --- 17 | 18 | # Create sample dashboards for the GitHub data source plugin for Grafana 19 | 20 | This page explains how you can create a sample dashboard in Grafana to get started with the GitHub data source plugin. You can obtain these sample dashboards by: 21 | 22 | - Using pre-configured dashboards 23 | - Using play demo 24 | 25 | ## Use pre-configured dashboards 26 | 27 | The pre-configured dashboards are ready-to-use and you only need to import them inside your Grafana server. There are two ways to use the pre-configure dashboards: 28 | 29 | - Importing from the official Website 30 | - Importing from the Grafana server WebUI 31 | 32 | ### Import from the Dashboards page on grafana.com 33 | 34 | Import the [GitHub Default dashboard](https://grafana.com/grafana/dashboards/14000). 35 | 36 | For instructions on how to import dashboards in Grafana, refer to [Import a dashboard](https://grafana.com/docs/grafana/latest/reference/export_import/#importing-a-dashboard). 37 | The dashboard ID is `1400`. 38 | 39 | ### Import in the Grafana UI 40 | 41 | To import a dashboard in the Grafana UI: 42 | 43 | 1. Go to **Connections** in the sidebar menu. 44 | 1. Under Connections, click **Data sources**. 45 | 1. Type `GitHub` in the search bar and select the GitHub data source. 46 | 1. Go to the **Dashboards** tab to view a list of pre-made dashboards. 47 | 1. Click **Import** to import the pre-made dashboard. 48 | 49 | ## Play demo 50 | 51 | The Play demo dashboards provides a reference dashboard and allows you to create your own custom dashboards. 52 | 53 | {{< docs/play title="GitHub data source plugin demo" url="https://play.grafana.org/dashboards/f/bb613d16-7ee5-4cf4-89ac-60dd9405fdd7/demo-github" >}} 54 | -------------------------------------------------------------------------------- /docs/sources/setup/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Set up the GitHub data source plugin for Grafana 3 | menuTitle: Setup 4 | description: Learn how to install and configure the GitHub data source plugin. 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 100 16 | --- 17 | 18 | # Set up the GitHub data source plugin for Grafana 19 | 20 | To set up the GitHub data source plugin for Grafana, refer to the following topics: 21 | 22 | {{< section menuTitle="true" withDescriptions="true" >}} 23 | -------------------------------------------------------------------------------- /docs/sources/setup/datasource.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configure the GitHub data source plugin for Grafana 3 | menuTitle: Configure 4 | description: Configure the GitHub data source plugin to authenticate to GitHub 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 103 16 | --- 17 | 18 | # Configure the GitHub data source plugin for Grafana 19 | 20 | 1. After creating the **access token** in GitHub, navigate into Grafana and click on the menu option on the top left. 21 | 22 | 1. Browse to the **Connections** menu and then click on the **Data sources**. 23 | 24 | 1. Click on the **Add new data source** button 25 | 26 | 1. Click on the GitHub data source plugin which you have installed. 27 | 28 | 1. Go to its settings tab and at the bottom, you will find the **Authentication** section. 29 | 30 | 1. Paste the access token. 31 | {{< figure alt="Configuring API Token" src="/media/docs/grafana/data-sources/github/github-plugin-confg-token.png" >}} 32 | 33 | 1. (_Optional_): If you using the GitHub Enterprise Server, then select the **Enterprise Server** option inside the **Connection** section and paste the URL of your GitHub Enterprise Server. 34 | 35 | 1. Click **Save & Test** button and you should see a confirmation dialog box that says "Data source is working". 36 | 37 | {{< admonition type="tip" >}} 38 | If you see errors, check the Grafana logs for troubleshooting. 39 | {{< /admonition >}} 40 | -------------------------------------------------------------------------------- /docs/sources/setup/provisioning.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Provisioning the GitHub data source in Grafana 3 | menuTitle: Provisioning 4 | description: Provisioning the GitHub data source plugin 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 104 16 | --- 17 | 18 | # Provisioning the GitHub data source in Grafana 19 | 20 | You can define and configure the GitHub data source in YAML files with Grafana provisioning. For more information about provisioning a data source, and for available configuration options, refer to [Provision Grafana](https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources). 21 | 22 | **Example** 23 | 24 | ```yaml 25 | apiVersion: 1 26 | 27 | datasources: 28 | - name: GitHub (Personal Access Token) 29 | type: grafana-github-datasource 30 | jsonData: 31 | selectedAuthType: personal-access-token 32 | secureJsonData: 33 | accessToken: 34 | 35 | - name: GitHub (App) 36 | type: grafana-github-datasource 37 | jsonData: 38 | selectedAuthType: github-app 39 | appId: 40 | installationId: 41 | secureJsonData: 42 | privateKey: 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/sources/variables-and-macros/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use GitHub data source variables and macros in Grafana 3 | menuTitle: Variables and macros 4 | description: Learn about variables and macros you can use in the GitHub data source plugin for Grafana 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 200 16 | --- 17 | 18 | # Use GitHub data source variables and macros in Grafana 19 | 20 | To learn more about variables and macros you can use in the GitHub data source plugin for Grafana, refer to the following topics: 21 | 22 | {{< section menuTitle="true" withDescriptions="true" >}} 23 | -------------------------------------------------------------------------------- /docs/sources/variables-and-macros/macros.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Use macros with GitHub data source plugin for Grafana 3 | menuTitle: Macros 4 | description: Learn about the macros you can use in the GitHub data source plugin for Grafana 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 202 16 | --- 17 | 18 | # Use macros with GitHub data source plugin for Grafana 19 | 20 | A macro is a feature that allows you to simplify the syntax and add dynamic parts to your queries. 21 | They help make your queries more flexible. 22 | 23 | The GitHub data source plugin for Grafana supports the following macros: 24 | 25 | | Macro name | Syntax | Description | Example | 26 | | ---------- | -------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | 27 | | multiVar | `$__multiVar(prefix,$var)` | Expands a multi value variable into github query string | `$__multiVar(label,$labels)` will expand into `label:first-label label:second-label` | 28 | | | | When using **all** in multi variable, use **\*** as custom all value | | 29 | | day | `$__toDay(diff)` | Returns the day according to UTC time, a difference in days can be added | `created:$__toDay(-7)` on 2022-01-17 will expand into `created:2022-01-10` | 30 | -------------------------------------------------------------------------------- /docs/sources/variables-and-macros/variables.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create variable with GitHub data source plugin for Grafana 3 | menuTitle: Variables 4 | description: Learn about the variables you can use in the GitHub data source plugin for Grafana 5 | keywords: 6 | - data source 7 | - github 8 | - github repository 9 | - API 10 | labels: 11 | products: 12 | - oss 13 | - enterprise 14 | - cloud 15 | weight: 201 16 | --- 17 | 18 | # Create variable with GitHub data source plugin for Grafana 19 | 20 | A [variable](https://grafana.com/docs/grafana/latest/variables/) is a placeholder for a value that you can use in dashboard queries. 21 | 22 | Variables allow you to create more interactive and dynamic dashboards by replacing hard-coded values with dynamic options. They are displayed as dropdown lists at the top of the dashboard, making it easy to change the data being displayed. 23 | 24 | **Example** 25 | 26 | Here is an example of creating a dashboard variable: 27 | 28 | {{< figure alt="Creating variables" src="/media/docs/grafana/data-sources/github/variables-create.png" >}} 29 | 30 | You can reference them inside queries, allowing users to configure parameters such as `Query` or `Repository`. 31 | 32 | {{< figure alt="Using variables inside queries" src="/media/docs/grafana/data-sources/github/using-variables.png" >}} 33 | -------------------------------------------------------------------------------- /docs/variables.mk: -------------------------------------------------------------------------------- 1 | PROJECTS := github-datasource 2 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | // Jest setup provided by Grafana scaffolding 2 | import './.config/jest-setup'; 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // force timezone to UTC to allow tests to work regardless of local timezone 2 | // generally used by snapshots, but can affect specific tests 3 | process.env.TZ = 'UTC'; 4 | 5 | module.exports = { 6 | // Jest configuration provided by Grafana scaffolding 7 | ...require('./.config/jest.config'), 8 | }; 9 | -------------------------------------------------------------------------------- /pkg/dfutil/framer.go: -------------------------------------------------------------------------------- 1 | package dfutil 2 | 3 | import ( 4 | "github.com/grafana/grafana-plugin-sdk-go/backend" 5 | "github.com/grafana/grafana-plugin-sdk-go/data" 6 | ) 7 | 8 | // Framer is an interface that allows any type to be treated as a data frame 9 | type Framer interface { 10 | Frames() data.Frames 11 | } 12 | 13 | // FrameResponse creates a backend.DataResponse that contains the Framer's data.Frames 14 | func FrameResponse(f Framer) backend.DataResponse { 15 | return backend.DataResponse{ 16 | Frames: f.Frames(), 17 | } 18 | } 19 | 20 | // FrameResponseWithError creates a backend.DataResponse with the error's contents (if not nil), and the Framer's data.Frames 21 | // This function is particularly useful if you have a function that returns `(Framer, error)`, which is a very common pattern 22 | func FrameResponseWithError(f Framer, err error) backend.DataResponse { 23 | if err != nil { 24 | if backend.IsDownstreamHTTPError(err) { 25 | err = backend.DownstreamError(err) 26 | } 27 | res := backend.ErrorResponseWithErrorSource(err) 28 | backend.Logger.Debug("Error response", "errorsource", res.ErrorSource, "error", res.Error) 29 | return res 30 | } 31 | 32 | return FrameResponse(f) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/errors/docs.go: -------------------------------------------------------------------------------- 1 | // Package dserrors contains common errors that functions will return in this project 2 | package dserrors 3 | -------------------------------------------------------------------------------- /pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package dserrors 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrorBadDatasource is only returned when the plugin instance's type could not be asserted 7 | ErrorBadDatasource = errors.New("instance from plugin context is not a GitHub Datasource") 8 | 9 | // ErrorQueryTypeUnimplemented is returned when the client sends an unrecognized querytype 10 | ErrorQueryTypeUnimplemented = errors.New("the query type provided is not implemented") 11 | 12 | // ErrorQueryTypeMissing is returned when the client does not send a query type 13 | ErrorQueryTypeMissing = errors.New("the query type was not provided in the URL") 14 | 15 | // ErrorTimeFieldNotSupported is returned when a time field sent is not supported / recognized. This can be returned when querying for any data that has multiple time fields, like Issues and Pull Requests 16 | ErrorTimeFieldNotSupported = errors.New("the selected time field is not supported") 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/github/client/errorsourcehandling.go: -------------------------------------------------------------------------------- 1 | package githubclient 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | googlegithub "github.com/google/go-github/v72/github" 10 | "github.com/grafana/grafana-plugin-sdk-go/backend" 11 | ) 12 | 13 | var statusErrorStringFromGraphQLPackage = "non-200 OK status code: " 14 | 15 | // Identified downstream errors. Unfortunately, could not find a better way to identify them. 16 | var ( 17 | downstreamErrors = []string{ 18 | "Could not resolve to", 19 | "Your token has not been granted the required scopes to execute this query", 20 | "Resource protected by organization SAML enforcement", 21 | "Resource not accessible by personal access token", 22 | "API rate limit exceeded", 23 | "Resource not accessible by integration", // issue with incorrectly set permissions for token/app 24 | } 25 | ) 26 | 27 | func addErrorSourceToError(err error, resp *googlegithub.Response) error { 28 | // If there is no error then return nil 29 | if err == nil { 30 | return nil 31 | } 32 | 33 | if backend.IsDownstreamHTTPError(err) { 34 | return backend.DownstreamError(err) 35 | } 36 | 37 | for _, downstreamError := range downstreamErrors { 38 | if strings.Contains(err.Error(), downstreamError) { 39 | return backend.DownstreamError(err) 40 | } 41 | } 42 | // Unfortunately graphql library that is used is not returning original error from the client. 43 | // It creates a new error with "non-200 OK status code: ..." error message. It includes status code 44 | // which we can extract and use. Mentioned code: https://github.com/shurcooL/graphql/blob/ed46e5a46466/graphql.go#L77. 45 | if strings.Contains(err.Error(), statusErrorStringFromGraphQLPackage) { 46 | statusCode, statusErr := extractStatusCode(err) 47 | if statusErr == nil { 48 | if backend.ErrorSourceFromHTTPStatus(statusCode) == backend.ErrorSourceDownstream { 49 | return backend.DownstreamError(err) 50 | } 51 | return backend.PluginError(err) 52 | } 53 | } 54 | // If we have response we can use the status code from it 55 | if resp != nil { 56 | if resp.StatusCode/100 != 2 { 57 | if backend.ErrorSourceFromHTTPStatus(resp.StatusCode) == backend.ErrorSourceDownstream { 58 | return backend.DownstreamError(err) 59 | } 60 | return backend.PluginError(err) 61 | } 62 | } 63 | // Otherwise we are not adding source which means it is going to be plugin error 64 | // not sure if this is the correct way to handle this as the error might be still coming 65 | // from the package that we are using. We should look into it once we have more data on this. 66 | return err 67 | } 68 | 69 | func extractStatusCode(err error) (int, error) { 70 | // Define the regular expression to match the numerical status code. 71 | re := regexp.MustCompile(statusErrorStringFromGraphQLPackage + `(\d{3})`) 72 | 73 | // Find the match in the error message. 74 | matches := re.FindStringSubmatch(err.Error()) 75 | if len(matches) > 1 { 76 | // Convert the captured group which contains the numerical status code to an integer. 77 | statusCode, conversionErr := strconv.Atoi(matches[1]) 78 | if conversionErr != nil { 79 | return 0, errors.New("failed to convert status code to integer") 80 | } 81 | return statusCode, nil 82 | } 83 | 84 | return 0, errors.New("status code not found in error message") 85 | } 86 | -------------------------------------------------------------------------------- /pkg/github/codescanning_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/backend" 7 | 8 | "github.com/grafana/github-datasource/pkg/dfutil" 9 | "github.com/grafana/github-datasource/pkg/models" 10 | ) 11 | 12 | func (s *QueryHandler) handleCodeScanningRequests(ctx context.Context, q backend.DataQuery) backend.DataResponse { 13 | query := &models.CodeScanningQuery{} 14 | if err := UnmarshalQuery(q.JSON, query); err != nil { 15 | return *err 16 | } 17 | return dfutil.FrameResponseWithError(s.Datasource.HandleCodeScanningQuery(ctx, query, q)) 18 | } 19 | 20 | // HandleCodeScanning handles the plugin query for github code scanning 21 | func (s *QueryHandler) HandleCodeScanning(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 22 | return &backend.QueryDataResponse{ 23 | Responses: processQueries(ctx, req, s.handleCodeScanningRequests), 24 | }, nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/github/commits_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleCommitsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.CommitsQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleCommitsQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleCommits handles the plugin query for github Commits 20 | func (s *QueryHandler) HandleCommits(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleCommitsQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/commits_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | func TestGetAllCommits(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | opts = models.ListCommitsOptions{ 17 | Repository: "test", 18 | Ref: "master", 19 | Owner: "kminehart-test", 20 | } 21 | ) 22 | 23 | testVariables := testutil.GetTestVariablesFunction("name", "owner", "ref") 24 | 25 | client := testutil.NewTestClient(t, 26 | testVariables, 27 | testutil.GetTestQueryFunction(&QueryListCommits{}), 28 | ) 29 | 30 | _, err := GetAllCommits(ctx, client, opts) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestListCommits(t *testing.T) { 37 | var ( 38 | ctx = context.Background() 39 | opts = models.ListCommitsOptions{ 40 | Repository: "grafana", 41 | Ref: "master", 42 | Owner: "grafana", 43 | } 44 | from = time.Now().Add(-7 * 24 * time.Hour) 45 | to = time.Now() 46 | ) 47 | 48 | testVariables := testutil.GetTestVariablesFunction("name", "owner", "ref", "cursor", "since", "until") 49 | 50 | client := testutil.NewTestClient(t, 51 | testVariables, 52 | testutil.GetTestQueryFunction(&QueryListCommitsInRange{}), 53 | ) 54 | 55 | _, err := GetCommitsInRange(ctx, client, opts, from, to) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | } 60 | 61 | func TestCommitsDataframe(t *testing.T) { 62 | committedAt, err := time.Parse(time.RFC3339, "2020-08-25T16:21:56+00:00") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | commits := Commits{ 68 | Commit{ 69 | OID: "", 70 | PushedDate: githubv4.DateTime{ 71 | Time: committedAt.Add(time.Minute * 2), 72 | }, 73 | AuthoredDate: githubv4.DateTime{ 74 | Time: committedAt, 75 | }, 76 | CommittedDate: githubv4.DateTime{ 77 | Time: committedAt, 78 | }, 79 | Message: "commit #1", 80 | Author: GitActor{ 81 | Name: "firstCommitter", 82 | Email: "first@example.com", 83 | User: models.User{ 84 | ID: "1", 85 | Login: "firstCommitter", 86 | Name: "First Committer", 87 | Company: "ACME Corp", 88 | Email: "first@example.com", 89 | }, 90 | }, 91 | }, 92 | Commit{ 93 | OID: "", 94 | PushedDate: githubv4.DateTime{ 95 | Time: committedAt.Add(time.Hour * 2), 96 | }, 97 | AuthoredDate: githubv4.DateTime{ 98 | Time: committedAt.Add(time.Hour), 99 | }, 100 | CommittedDate: githubv4.DateTime{ 101 | Time: committedAt.Add(time.Hour), 102 | }, 103 | Message: "commit #2", 104 | Author: GitActor{ 105 | Name: "secondCommitter", 106 | Email: "second@example.com", 107 | User: models.User{ 108 | ID: "1", 109 | Login: "secondCommitter", 110 | Name: "Second Committer", 111 | Company: "ACME Corp", 112 | Email: "second@example.com", 113 | }, 114 | }, 115 | }, 116 | } 117 | 118 | testutil.CheckGoldenFramer(t, "commits", commits) 119 | } 120 | -------------------------------------------------------------------------------- /pkg/github/constants.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | // PageNumberLimit is the limit on the number of pages that will be traversed 4 | const PageNumberLimit = 2 5 | -------------------------------------------------------------------------------- /pkg/github/contributors_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleContributorsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.ContributorsQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleContributorsQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleContributors handles the plugin query for github Contributors 20 | func (s *QueryHandler) HandleContributors(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleContributorsQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/contributors_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/github-datasource/pkg/testutil" 9 | ) 10 | 11 | func TestGetAllContributors(t *testing.T) { 12 | var ( 13 | ctx = context.Background() 14 | opts = models.ListContributorsOptions{ 15 | Repository: "grafana", 16 | Owner: "grafana", 17 | } 18 | ) 19 | 20 | testVariables := testutil.GetTestVariablesFunction("name", "owner") 21 | 22 | client := testutil.NewTestClient(t, 23 | testVariables, 24 | testutil.GetTestQueryFunction(&QueryListContributors{}), 25 | ) 26 | 27 | _, err := GetAllContributors(ctx, client, opts) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | 33 | func TestGetContributorsWithQuery(t *testing.T) { 34 | var ( 35 | ctx = context.Background() 36 | q = "test query" 37 | opts = models.ListContributorsOptions{ 38 | Repository: "grafana", 39 | Owner: "grafana", 40 | Query: &q, 41 | } 42 | ) 43 | 44 | testVariables := testutil.GetTestVariablesFunction("name", "owner", "query") 45 | 46 | client := testutil.NewTestClient(t, 47 | testVariables, 48 | testutil.GetTestQueryFunction(&QueryListContributors{}), 49 | ) 50 | 51 | _, err := GetAllContributors(ctx, client, opts) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | 57 | func TestContributorsDataFrame(t *testing.T) { 58 | contributors := GitActors{ 59 | GitActor{ 60 | Name: "Example User", 61 | Email: "user1@example.com", 62 | User: models.User{ 63 | Login: "exUser1", 64 | Name: "Example User", 65 | Company: "ACME Corp", 66 | Email: "user1@example.com", 67 | URL: "https://github.com/user1", 68 | }, 69 | }, 70 | GitActor{ 71 | Name: "Example User2", 72 | Email: "user2@example.com", 73 | User: models.User{ 74 | Login: "exUser2", 75 | Name: "Example User2", 76 | Company: "ACME Corp", 77 | Email: "user2@example.com", 78 | URL: "https://github.com/user2", 79 | }, 80 | }, 81 | } 82 | 83 | testutil.CheckGoldenFramer(t, "contributors", contributors) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/github/docs.go: -------------------------------------------------------------------------------- 1 | // Package github contains more usable functions and types for interacting with the GitHubv4 API 2 | package github 3 | -------------------------------------------------------------------------------- /pkg/github/issues_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleIssuesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.IssuesQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleIssuesQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleIssues handles the plugin query for github Issues 20 | func (s *QueryHandler) HandleIssues(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleIssuesQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/labels.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/models" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | "github.com/pkg/errors" 9 | "github.com/shurcooL/githubv4" 10 | ) 11 | 12 | // QueryListLabels lists all labels in a repository 13 | // { 14 | // repository(name: "grafana", owner: "grafana") { 15 | // labels(first: 100) { 16 | // nodes { 17 | // color 18 | // description 19 | // name 20 | // } 21 | // } 22 | // } 23 | // } 24 | type QueryListLabels struct { 25 | Repository struct { 26 | Labels struct { 27 | Nodes Labels 28 | PageInfo models.PageInfo 29 | } `graphql:"labels(first: 100, after: $cursor, query: $query)"` 30 | } `graphql:"repository(name: $name, owner: $owner)"` 31 | } 32 | 33 | // Label is a GitHub label used in Issues / Pull Requests 34 | type Label struct { 35 | Color string `json:"color"` 36 | Name string `json:"name"` 37 | Description string `json:"description"` 38 | } 39 | 40 | // Labels is a list of GitHub labels 41 | type Labels []Label 42 | 43 | // Frames converts the list of labels to a Grafana DataFrame 44 | func (a Labels) Frames() data.Frames { 45 | frame := data.NewFrame( 46 | "labels", 47 | data.NewField("color", nil, []string{}), 48 | data.NewField("name", nil, []string{}), 49 | data.NewField("description", nil, []string{}), 50 | ) 51 | 52 | for _, v := range a { 53 | frame.AppendRow( 54 | v.Color, 55 | v.Name, 56 | v.Description, 57 | ) 58 | } 59 | 60 | return data.Frames{frame} 61 | } 62 | 63 | // GetAllLabels gets all labels from a GitHub repository 64 | func GetAllLabels(ctx context.Context, client models.Client, opts models.ListLabelsOptions) (Labels, error) { 65 | queryString, err := InterPolateMacros(opts.Query) 66 | if err != nil { 67 | return nil, errors.WithStack(err) 68 | } 69 | var ( 70 | variables = map[string]interface{}{ 71 | "cursor": (*githubv4.String)(nil), 72 | "query": githubv4.String(queryString), 73 | "owner": githubv4.String(opts.Owner), 74 | "name": githubv4.String(opts.Repository), 75 | } 76 | 77 | labels = Labels{} 78 | ) 79 | 80 | for { 81 | q := &QueryListLabels{} 82 | if err := client.Query(ctx, q, variables); err != nil { 83 | return nil, errors.WithStack(err) 84 | } 85 | 86 | labels = append(labels, q.Repository.Labels.Nodes...) 87 | 88 | if !q.Repository.Labels.PageInfo.HasNextPage { 89 | break 90 | } 91 | variables["cursor"] = q.Repository.Labels.PageInfo.EndCursor 92 | } 93 | 94 | return labels, nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/github/labels_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleLabelsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.LabelsQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleLabelsQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleLabels handles the plugin query for github Labels 20 | func (s *QueryHandler) HandleLabels(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleLabelsQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/labels_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/github-datasource/pkg/testutil" 9 | ) 10 | 11 | func TestListLabels(t *testing.T) { 12 | var ( 13 | ctx = context.Background() 14 | opts = models.ListLabelsOptions{ 15 | Repository: "grafana", 16 | Owner: "grafana", 17 | Query: "test", 18 | } 19 | ) 20 | 21 | testVariables := testutil.GetTestVariablesFunction("query", "name", "owner", "cursor") 22 | 23 | client := testutil.NewTestClient(t, 24 | testVariables, 25 | testutil.GetTestQueryFunction(&QueryListLabels{}), 26 | ) 27 | 28 | _, err := GetAllLabels(ctx, client, opts) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pkg/github/macros.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type macroFunc func(string, []string) (string, error) 13 | 14 | func getMatches(macroName, rawSQL string) ([][]string, error) { 15 | macroRegex := fmt.Sprintf("\\$__%s\\b(?:\\((.*?)\\))?", macroName) 16 | rgx, err := regexp.Compile(macroRegex) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return rgx.FindAllStringSubmatch(rawSQL, -1), nil 21 | } 22 | 23 | func trimAll(s []string) []string { 24 | r := make([]string, len(s)) 25 | for i, v := range s { 26 | r[i] = strings.TrimSpace(v) 27 | } 28 | return r 29 | } 30 | 31 | // InterPolateMacros interpolate macros on a given query string 32 | func InterPolateMacros(query string) (string, error) { 33 | macros := map[string]macroFunc{ 34 | "multiVar": func(query string, args []string) (string, error) { 35 | out := "" 36 | prop := "" 37 | if len(args) <= 1 { 38 | return query, errors.New("insufficient arguments to multiVar") 39 | } 40 | if len(args) == 2 && args[1] == "*" { 41 | return "", nil 42 | } 43 | for idx, arg := range args { 44 | if idx == 0 { 45 | prop = arg 46 | continue 47 | } 48 | out = strings.Trim(fmt.Sprintf("%s %s:%s", out, prop, arg), " ") 49 | } 50 | return out, nil 51 | }, 52 | "toDay": func(query string, args []string) (string, error) { 53 | diff := 0 54 | if args[0] != "" { 55 | var err error 56 | diff, err = strconv.Atoi(args[0]) 57 | if err != nil { 58 | return query, errors.New("argument for day is not an integer") 59 | } 60 | } 61 | expectedDay := time.Now().UTC().AddDate(0, 0, diff) 62 | return expectedDay.Format("2006-01-02"), nil 63 | }, 64 | } 65 | for key, macro := range macros { 66 | matches, err := getMatches(key, query) 67 | if err != nil { 68 | return query, err 69 | } 70 | for _, match := range matches { 71 | if len(match) == 0 { 72 | continue 73 | } 74 | args := []string{} 75 | if len(match) > 1 { 76 | args = trimAll(strings.Split(match[1], ",")) 77 | } 78 | res, err := macro(query, args) 79 | if err != nil { 80 | return query, err 81 | } 82 | query = strings.Replace(query, match[0], res, -1) 83 | } 84 | } 85 | return strings.Trim(query, " "), nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/github/macros_test.go: -------------------------------------------------------------------------------- 1 | package github_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/github" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestInterPolateMacros(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | query string 16 | want string 17 | wantErr error 18 | }{ 19 | {query: ""}, 20 | {query: "hello world", want: "hello world"}, 21 | {query: "hello $saturn", want: "hello $saturn"}, 22 | {query: "hello $__multiVar()", wantErr: errors.New("insufficient arguments to multiVar")}, 23 | {query: "hello $__multiVar(repo)", wantErr: errors.New("insufficient arguments to multiVar")}, 24 | {query: "hello $__multiVar(repo,*)", want: "hello"}, 25 | {query: "hello $__multiVar(repo,*) world", want: "hello world"}, 26 | {query: "hello $__multiVar(repo,a,b,c)", want: "hello repo:a repo:b repo:c"}, 27 | {query: "hello $__multiVar(repo,a,b,c) $__multiVar(label,c,b,a) world", want: "hello repo:a repo:b repo:c label:c label:b label:a world"}, 28 | {query: "created:$__toDay(today)", wantErr: errors.New("argument for day is not an integer")}, 29 | {query: "created:$__toDay()", want: "created:" + time.Now().UTC().Format("2006-01-02")}, 30 | {query: "$__toDay(0)", want: time.Now().UTC().Format("2006-01-02")}, 31 | {query: "$__toDay(1)", want: time.Now().UTC().AddDate(0, 0, 1).Format("2006-01-02")}, 32 | {query: "$__toDay(-1)", want: time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02")}, 33 | {query: "$__toDay(-14)..$__toDay(-7)", want: time.Now().UTC().AddDate(0, 0, -14).Format("2006-01-02") + ".." + time.Now().UTC().AddDate(0, 0, -7).Format("2006-01-02")}, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | got, err := github.InterPolateMacros(tt.query) 38 | if tt.wantErr != nil { 39 | assert.Equal(t, tt.wantErr, err) 40 | return 41 | } 42 | assert.Nil(t, err) 43 | assert.Equal(t, tt.want, got) 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/github/milestones.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/data" 9 | "github.com/pkg/errors" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | // QueryListMilestones lists all milestones in a repository 14 | // { 15 | // repository(name: "grafana", owner: "grafana") { 16 | // milestones(first: 100) { 17 | // nodes { 18 | // color 19 | // description 20 | // name 21 | // } 22 | // } 23 | // } 24 | // } 25 | type QueryListMilestones struct { 26 | Repository struct { 27 | Milestones struct { 28 | Nodes Milestones 29 | PageInfo models.PageInfo 30 | } `graphql:"milestones(first: 100, after: $cursor, query: $query)"` 31 | } `graphql:"repository(name: $name, owner: $owner)"` 32 | } 33 | 34 | // Milestones is a list of GitHub milestones 35 | type Milestones []models.Milestone 36 | 37 | // Frames converts the list of GitHub Milestones to a Grafana data frame 38 | func (m Milestones) Frames() data.Frames { 39 | frame := data.NewFrame( 40 | "milestones", 41 | data.NewField("title", nil, []string{}), 42 | data.NewField("author", nil, []string{}), 43 | data.NewField("closed", nil, []bool{}), 44 | data.NewField("state", nil, []string{}), 45 | data.NewField("created_at", nil, []time.Time{}), 46 | data.NewField("closed_at", nil, []*time.Time{}), 47 | data.NewField("due_at", nil, []*time.Time{}), 48 | ) 49 | 50 | for _, v := range m { 51 | var ( 52 | closedAt *time.Time 53 | dueAt *time.Time 54 | ) 55 | if !v.ClosedAt.Time.IsZero() { 56 | t := v.ClosedAt.Time 57 | closedAt = &t 58 | } 59 | 60 | if !v.DueOn.Time.IsZero() { 61 | t := v.DueOn.Time 62 | dueAt = &t 63 | } 64 | 65 | frame.AppendRow( 66 | v.Title, 67 | v.Creator.User.Login, 68 | v.Closed, 69 | string(v.State), 70 | v.CreatedAt.Time, 71 | closedAt, 72 | dueAt, 73 | ) 74 | } 75 | 76 | return data.Frames{frame} 77 | } 78 | 79 | // GetAllMilestones lists milestones in a repository 80 | func GetAllMilestones(ctx context.Context, client models.Client, opts models.ListMilestonesOptions) (Milestones, error) { 81 | queryString, err := InterPolateMacros(opts.Query) 82 | if err != nil { 83 | return nil, errors.WithStack(err) 84 | } 85 | var ( 86 | variables = map[string]interface{}{ 87 | "cursor": (*githubv4.String)(nil), 88 | "query": githubv4.String(queryString), 89 | "owner": githubv4.String(opts.Owner), 90 | "name": githubv4.String(opts.Repository), 91 | } 92 | 93 | milestones = Milestones{} 94 | ) 95 | 96 | for { 97 | q := &QueryListMilestones{} 98 | if err := client.Query(ctx, q, variables); err != nil { 99 | return nil, errors.WithStack(err) 100 | } 101 | 102 | milestones = append(milestones, q.Repository.Milestones.Nodes...) 103 | 104 | if !q.Repository.Milestones.PageInfo.HasNextPage { 105 | break 106 | } 107 | variables["cursor"] = q.Repository.Milestones.PageInfo.EndCursor 108 | } 109 | 110 | return milestones, nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/github/milestones_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleMilestonesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.MilestonesQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleMilestonesQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleMilestones handles the plugin query for github Milestones 20 | func (s *QueryHandler) HandleMilestones(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleMilestonesQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/milestones_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | func TestListMilestones(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | opts = models.ListMilestonesOptions{ 17 | Repository: "grafana", 18 | Owner: "grafana", 19 | Query: "test", 20 | } 21 | ) 22 | 23 | testVariables := testutil.GetTestVariablesFunction("query", "name", "owner", "cursor") 24 | 25 | client := testutil.NewTestClient(t, 26 | testVariables, 27 | testutil.GetTestQueryFunction(&QueryListMilestones{}), 28 | ) 29 | 30 | _, err := GetAllMilestones(ctx, client, opts) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestMilestonesDataframe(t *testing.T) { 37 | openedAt, err := time.Parse(time.RFC3339, "2020-08-25T16:21:56+00:00") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | firstUser := models.User{ 43 | ID: "1", 44 | Login: "testUser", 45 | Name: "Test User", 46 | Company: "ACME corp", 47 | Email: "user@example.com", 48 | } 49 | secondUser := models.User{ 50 | ID: "2", 51 | Login: "testUser2", 52 | Name: "Second User", 53 | Company: "ACME corp", 54 | Email: "user2@example.com", 55 | } 56 | 57 | milestones := Milestones{ 58 | { 59 | Closed: false, 60 | Creator: struct { 61 | User models.User "graphql:\"... on User\"" 62 | }{ 63 | User: firstUser, 64 | }, 65 | DueOn: githubv4.DateTime{ 66 | Time: openedAt.Add(100 * time.Hour), 67 | }, 68 | ClosedAt: githubv4.DateTime{}, 69 | CreatedAt: githubv4.DateTime{ 70 | Time: openedAt, 71 | }, 72 | State: githubv4.MilestoneStateOpen, 73 | Title: "first milestone", 74 | }, 75 | { 76 | Closed: true, 77 | Creator: struct { 78 | User models.User "graphql:\"... on User\"" 79 | }{ 80 | User: secondUser, 81 | }, 82 | DueOn: githubv4.DateTime{ 83 | Time: openedAt.Add(100 * time.Hour), 84 | }, 85 | ClosedAt: githubv4.DateTime{ 86 | Time: openedAt.Add(10 * time.Hour), 87 | }, 88 | CreatedAt: githubv4.DateTime{ 89 | Time: openedAt, 90 | }, 91 | State: githubv4.MilestoneStateClosed, 92 | Title: "second milestone", 93 | }, 94 | { 95 | Closed: false, 96 | Creator: struct { 97 | User models.User "graphql:\"... on User\"" 98 | }{ 99 | User: secondUser, 100 | }, 101 | DueOn: githubv4.DateTime{ 102 | Time: openedAt.Add(120 * time.Hour), 103 | }, 104 | ClosedAt: githubv4.DateTime{}, 105 | CreatedAt: githubv4.DateTime{ 106 | Time: openedAt, 107 | }, 108 | State: githubv4.MilestoneStateOpen, 109 | Title: "third milestone", 110 | }, 111 | } 112 | 113 | testutil.CheckGoldenFramer(t, "milestones", milestones) 114 | } 115 | -------------------------------------------------------------------------------- /pkg/github/organizations.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/models" 7 | "github.com/grafana/grafana-plugin-sdk-go/data" 8 | "github.com/shurcooL/githubv4" 9 | ) 10 | 11 | // An Organization is a single GitHub organization 12 | type Organization struct { 13 | Name string 14 | } 15 | 16 | // Organizations is a slice of GitHub Organizations 17 | type Organizations []Organization 18 | 19 | // Frames converts the list of Organizations to a Grafana DataFrame 20 | func (c Organizations) Frames() data.Frames { 21 | return data.Frames{} 22 | } 23 | 24 | // QueryListOrganizations is the GraphQL query for listing organizations 25 | type QueryListOrganizations struct { 26 | Viewer struct { 27 | Organizations struct { 28 | Nodes []Organization 29 | PageInfo models.PageInfo 30 | } `graphql:"organizations(first: 100, after: $cursor)"` 31 | } 32 | } 33 | 34 | // GetAllOrganizations lists the available organizations for the client 35 | func GetAllOrganizations(ctx context.Context, client models.Client) ([]Organization, error) { 36 | var ( 37 | variables = map[string]interface{}{ 38 | "cursor": (*githubv4.String)(nil), 39 | } 40 | 41 | organizations = []Organization{} 42 | ) 43 | 44 | for { 45 | q := &QueryListOrganizations{} 46 | if err := client.Query(ctx, q, variables); err != nil { 47 | return nil, err 48 | } 49 | organizations = append(organizations, q.Viewer.Organizations.Nodes...) 50 | if !q.Viewer.Organizations.PageInfo.HasNextPage { 51 | break 52 | } 53 | variables["cursor"] = q.Viewer.Organizations.PageInfo.EndCursor 54 | } 55 | 56 | return organizations, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/github/organizations_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/grafana/github-datasource/pkg/testutil" 8 | ) 9 | 10 | func TestGetAllOrganizations(t *testing.T) { 11 | var ( 12 | ctx = context.Background() 13 | ) 14 | 15 | testVariables := func(t *testing.T, variables map[string]interface{}) { 16 | } 17 | 18 | client := testutil.NewTestClient(t, 19 | testVariables, 20 | testutil.GetTestQueryFunction(&QueryListOrganizations{}), 21 | ) 22 | 23 | _, err := GetAllOrganizations(ctx, client) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/github/packages_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handlePackagesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.PackagesQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandlePackagesQuery(ctx, query, q)) 17 | } 18 | 19 | // HandlePackages handles the plugin query for github Packages 20 | func (s *QueryHandler) HandlePackages(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handlePackagesQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/packages_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | -------------------------------------------------------------------------------- /pkg/github/projects/project_items_filter.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/araddon/dateparse" 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/grafana-plugin-sdk-go/backend" 10 | ) 11 | 12 | // filter checks if the values match the filter criteria 13 | func filter(fieldValue map[string]any, filters []models.Filter) bool { 14 | var conj string 15 | multi := len(filters) > 1 16 | allMatch := false 17 | if multi { 18 | conj = filters[0].Conjunction 19 | if conj == "and" || conj == "" { 20 | allMatch = true 21 | conj = "and" 22 | } 23 | } 24 | for _, f := range filters { 25 | val := fieldValue[f.Key] 26 | match := match(f.Value, val, f.OP) 27 | if match && !multi { 28 | return true 29 | } 30 | if match && conj == "or" { 31 | return true 32 | } 33 | if !match && conj == "and" { 34 | return false 35 | } 36 | } 37 | return allMatch 38 | } 39 | 40 | // match based on operator 41 | func match(v1 string, v2 any, op string) bool { 42 | switch op { 43 | case ">": 44 | return greaterThan(v1, v2) 45 | case "<": 46 | return lessThan(v1, v2) 47 | case "=": 48 | return equals(v1, v2) 49 | case "!=": 50 | return !equals(v1, v2) 51 | case ">=": 52 | return equals(v1, v2) || greaterThan(v1, v2) 53 | case "<=": 54 | return equals(v1, v2) || lessThan(v1, v2) 55 | case "~": 56 | return contains(v1, v2) 57 | } 58 | return false 59 | } 60 | 61 | func equals(v1 string, v2 any) bool { 62 | switch v := v2.(type) { 63 | case *string: 64 | return v1 == *v 65 | case string: 66 | return v1 == v 67 | case *time.Time: 68 | f := func(t time.Time) bool { return v != nil && v.Equal(t) } 69 | return checkDate(v1, f) 70 | case time.Time: 71 | f := func(t time.Time) bool { return v.Equal(t) } 72 | return checkDate(v1, f) 73 | } 74 | return false 75 | } 76 | 77 | func greaterThan(v1 string, v2 any) bool { 78 | switch v := v2.(type) { 79 | case *string: 80 | return v1 > *v 81 | case string: 82 | return v1 > v 83 | case *time.Time: 84 | f := func(t time.Time) bool { return v != nil && v.After(t) } 85 | return checkDate(v1, f) 86 | case time.Time: 87 | f := func(t time.Time) bool { return v.After(t) } 88 | return checkDate(v1, f) 89 | } 90 | return false 91 | } 92 | 93 | func lessThan(v1 string, v2 any) bool { 94 | switch v := v2.(type) { 95 | case *string: 96 | return v1 < *v 97 | case string: 98 | return v1 < v 99 | case *time.Time: 100 | f := func(t time.Time) bool { return v != nil && v.Before(t) } 101 | return checkDate(v1, f) 102 | case time.Time: 103 | f := func(t time.Time) bool { return v.Before(t) } 104 | return checkDate(v1, f) 105 | } 106 | return false 107 | } 108 | 109 | func contains(v1 string, v2 any) bool { 110 | switch v := v2.(type) { 111 | case *string: 112 | return strings.Contains(*v, v1) 113 | case string: 114 | return strings.Contains(v, v1) 115 | } 116 | return false 117 | } 118 | 119 | func checkDate(d string, f func(t time.Time) bool) bool { 120 | t, err := dateparse.ParseAny(d) 121 | if err != nil { 122 | backend.Logger.Error("Failed to parse date "+d, err) 123 | return false 124 | } 125 | return f(t) 126 | } 127 | -------------------------------------------------------------------------------- /pkg/github/projects/project_items_filter_test.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/grafana/github-datasource/pkg/models" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestOrFiltersShouldMatchOne(t *testing.T) { 11 | values := map[string]any{ 12 | "foo": "bar", 13 | } 14 | filter1 := models.Filter{ 15 | Key: "foo", 16 | Value: "bar", 17 | Conjunction: "or", 18 | OP: "=", 19 | } 20 | filter2 := models.Filter{ 21 | Key: "foo", 22 | Value: "baz", 23 | Conjunction: "or", 24 | OP: "=", 25 | } 26 | match := filter(values, []models.Filter{filter1, filter2}) 27 | assert.True(t, match) 28 | } 29 | 30 | func TestAndFiltersShouldMatchAll(t *testing.T) { 31 | values := map[string]any{ 32 | "foo": "bar", 33 | } 34 | filter1 := models.Filter{ 35 | Key: "foo", 36 | Value: "bar", 37 | Conjunction: "and", 38 | OP: "=", 39 | } 40 | filter2 := models.Filter{ 41 | Key: "foo", 42 | Value: "baz", 43 | Conjunction: "and", 44 | OP: "=", 45 | } 46 | match := filter(values, []models.Filter{filter1, filter2}) 47 | assert.False(t, match) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/github/projects/project_items_test.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | func TestGetAllProjectItems(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | opts = models.ProjectOptions{ 17 | Organization: "grafana", 18 | Number: 1, 19 | } 20 | ) 21 | 22 | testVariables := testutil.GetTestVariablesFunction("login", "number") 23 | 24 | client := testutil.NewTestClient(t, 25 | testVariables, 26 | testutil.GetTestQueryFunction(&QueryProject{}), 27 | ) 28 | 29 | _, err := GetAllProjectItems(ctx, client, opts) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | 35 | func TestProjectItemsDataFrame(t *testing.T) { 36 | 37 | dateString := "2021-11-22" 38 | date, err := time.Parse("2006-01-02", dateString) 39 | if err != nil { 40 | t.Fail() 41 | return 42 | } 43 | 44 | test := "Test" 45 | project := ProjectItemsWithFields{ 46 | Items: []ProjectItem{ 47 | { 48 | ProjectV2ItemContent{ 49 | Issue: IssueContent{ 50 | Title: &test, 51 | }, 52 | }, 53 | FieldValues{ 54 | Nodes: []FieldValue{ 55 | { 56 | TextValue: ProjectV2ItemFieldTextValue{ 57 | Text: &test, 58 | Field: CommonField{ 59 | Common: ProjectV2FieldCommon{ 60 | Name: "Field1", 61 | DataType: "TEXT", 62 | }, 63 | }, 64 | }, 65 | DateValue: ProjectV2ItemFieldDateValue{ 66 | Field: CommonField{ 67 | Common: ProjectV2FieldCommon{ 68 | Name: "Field1", 69 | DataType: "TEXT", 70 | }, 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | "Foo", 77 | false, 78 | "ISSUE", 79 | githubv4.DateTime{Time: date}, 80 | githubv4.DateTime{Time: date}, 81 | }, 82 | }, 83 | Fields: []Field{ 84 | { 85 | Common: ProjectV2FieldCommon{ 86 | Name: "Field1", 87 | DataType: "TEXT", 88 | }, 89 | }, 90 | }, 91 | Filters: []models.Filter{}, 92 | } 93 | 94 | testutil.CheckGoldenFramer(t, "project", project) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/github/projects/projects_test.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | func TestGetAllProjects(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | opts = models.ProjectOptions{ 17 | Organization: "grafana", 18 | } 19 | ) 20 | 21 | testVariables := testutil.GetTestVariablesFunction("login") 22 | 23 | client := testutil.NewTestClient(t, 24 | testVariables, 25 | testutil.GetTestQueryFunction(&QueryListProjects{}), 26 | ) 27 | 28 | _, err := GetAllProjects(ctx, client, opts) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | 34 | func TestProjectsDataFrame(t *testing.T) { 35 | 36 | dateString := "2021-11-22" 37 | date, err := time.Parse("2006-01-02", dateString) 38 | if err != nil { 39 | t.Fail() 40 | return 41 | } 42 | 43 | projects := Projects{ 44 | Project{ 45 | Number: 1, 46 | Title: "foo", 47 | CreatedAt: githubv4.DateTime{Time: date}, 48 | UpdatedAt: githubv4.DateTime{Time: date}, 49 | }, 50 | } 51 | 52 | testutil.CheckGoldenFramer(t, "projects", projects) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/github/projects_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleProjectsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.ProjectsQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleProjectsQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleProjects handles the plugin query for github Projects 20 | func (s *QueryHandler) HandleProjects(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleProjectsQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/pull_requests_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handlePullRequestsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.PullRequestsQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandlePullRequestsQuery(ctx, query, q)) 17 | } 18 | 19 | // HandlePullRequests handles the plugin query for github PullRequests 20 | func (s *QueryHandler) HandlePullRequests(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handlePullRequestsQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/query_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/grafana/github-datasource/pkg/models" 12 | ) 13 | 14 | // QueryHandler is the main handler for datasource queries. 15 | type QueryHandler struct { 16 | Datasource Datasource 17 | } 18 | 19 | // QueryHandlerFunc is the function signature used for mux.HandleFunc 20 | type QueryHandlerFunc func(context.Context, backend.DataQuery) backend.DataResponse 21 | 22 | func processQueries(ctx context.Context, req *backend.QueryDataRequest, handler QueryHandlerFunc) backend.Responses { 23 | res := backend.Responses{} 24 | for _, v := range req.Queries { 25 | res[v.RefID] = handler(ctx, v) 26 | } 27 | 28 | return res 29 | } 30 | 31 | // UnmarshalQuery attempts to unmarshal a query from JSON 32 | func UnmarshalQuery(b []byte, v interface{}) *backend.DataResponse { 33 | if err := json.Unmarshal(b, v); err != nil { 34 | return &backend.DataResponse{ 35 | Error: errors.Wrap(err, "failed to unmarshal JSON request into query"), 36 | ErrorSource: backend.ErrorSourceDownstream, 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | // GetQueryHandlers creates the QueryTypeMux type for handling queries 43 | func GetQueryHandlers(s *QueryHandler) *datasource.QueryTypeMux { 44 | mux := datasource.NewQueryTypeMux() 45 | 46 | // This could be a map[models.QueryType]datasource.QueryHandlerFunc and then a loop to handle all of them. 47 | mux.HandleFunc(models.QueryTypeCommits, s.HandleCommits) 48 | mux.HandleFunc(models.QueryTypeIssues, s.HandleIssues) 49 | mux.HandleFunc(models.QueryTypeContributors, s.HandleContributors) 50 | mux.HandleFunc(models.QueryTypeLabels, s.HandleLabels) 51 | mux.HandleFunc(models.QueryTypePullRequests, s.HandlePullRequests) 52 | mux.HandleFunc(models.QueryTypeReleases, s.HandleReleases) 53 | mux.HandleFunc(models.QueryTypeTags, s.HandleTags) 54 | mux.HandleFunc(models.QueryTypePackages, s.HandlePackages) 55 | mux.HandleFunc(models.QueryTypeMilestones, s.HandleMilestones) 56 | mux.HandleFunc(models.QueryTypeRepositories, s.HandleRepositories) 57 | mux.HandleFunc(models.QueryTypeVulnerabilities, s.HandleVulnerabilities) 58 | mux.HandleFunc(models.QueryTypeProjects, s.HandleProjects) 59 | mux.HandleFunc(models.QueryTypeStargazers, s.HandleStargazers) 60 | mux.HandleFunc(models.QueryTypeWorkflows, s.HandleWorkflows) 61 | mux.HandleFunc(models.QueryTypeWorkflowUsage, s.HandleWorkflowUsage) 62 | mux.HandleFunc(models.QueryTypeWorkflowRuns, s.HandleWorkflowRuns) 63 | mux.HandleFunc(models.QueryTypeCodeScanning, s.HandleCodeScanning) 64 | 65 | return mux 66 | } 67 | -------------------------------------------------------------------------------- /pkg/github/releases_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleReleasesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.ReleasesQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleReleasesQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleReleases handles the plugin query for github Releases 20 | func (s *QueryHandler) HandleReleases(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleReleasesQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/releases_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | func TestGetAllReleases(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | opts = models.ListReleasesOptions{ 17 | Repository: "grafana", 18 | Owner: "grafana", 19 | } 20 | ) 21 | 22 | testVariables := testutil.GetTestVariablesFunction("name", "owner") 23 | 24 | client := testutil.NewTestClient(t, 25 | testVariables, 26 | testutil.GetTestQueryFunction(&QueryListReleases{}), 27 | ) 28 | 29 | _, err := GetAllReleases(ctx, client, opts) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | 35 | func TestListReleases(t *testing.T) { 36 | var ( 37 | ctx = context.Background() 38 | opts = models.ListReleasesOptions{ 39 | Repository: "grafana", 40 | Owner: "grafana", 41 | } 42 | ) 43 | 44 | testVariables := testutil.GetTestVariablesFunction("name", "owner") 45 | 46 | client := testutil.NewTestClient(t, 47 | testVariables, 48 | testutil.GetTestQueryFunction(&QueryListReleases{}), 49 | ) 50 | 51 | _, err := GetReleasesInRange(ctx, client, opts, time.Now().Add(-30*24*time.Hour), time.Now()) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | 57 | func TestReleasesDataFrame(t *testing.T) { 58 | createdAt, err := time.Parse(time.RFC3339, "2020-08-25T16:21:56+00:00") 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | user := models.User{ 64 | ID: "1", 65 | Login: "exampleUser", 66 | Name: "Example User", 67 | Company: "ACME Corp", 68 | Email: "user@example.com", 69 | } 70 | 71 | releases := Releases{ 72 | Release{ 73 | ID: "1", 74 | Name: "Release #1", 75 | Author: user, 76 | IsDraft: true, 77 | IsPrerelease: false, 78 | CreatedAt: githubv4.DateTime{ 79 | Time: createdAt, 80 | }, 81 | PublishedAt: githubv4.DateTime{}, 82 | TagName: "v1.0.0", 83 | URL: "https://example.com/v1.0.0", 84 | }, 85 | Release{ 86 | ID: "1", 87 | Name: "Release #2", 88 | Author: user, 89 | IsDraft: true, 90 | IsPrerelease: false, 91 | CreatedAt: githubv4.DateTime{ 92 | Time: createdAt, 93 | }, 94 | PublishedAt: githubv4.DateTime{ 95 | Time: createdAt.Add(time.Hour), 96 | }, 97 | TagName: "v1.1.0", 98 | URL: "https://example.com/v1.1.0", 99 | }, 100 | } 101 | 102 | testutil.CheckGoldenFramer(t, "releases", releases) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/github/repositories.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/grafana/github-datasource/pkg/models" 10 | "github.com/grafana/grafana-plugin-sdk-go/data" 11 | "github.com/shurcooL/githubv4" 12 | ) 13 | 14 | // QueryListRepositories is the GraphQL query for retrieving a list of repositories for an organization 15 | // { 16 | // search(query: "is:pr repo:grafana/grafana merged:2020-08-19..*", type: ISSUE, first: 100) { 17 | // nodes { 18 | // ... on PullRequest { 19 | // id 20 | // title 21 | // } 22 | // } 23 | // } 24 | type QueryListRepositories struct { 25 | Search struct { 26 | Nodes []struct { 27 | Repository Repository `graphql:"... on Repository"` 28 | } 29 | PageInfo models.PageInfo 30 | } `graphql:"search(query: $query, type: REPOSITORY, first: 100, after: $cursor)"` 31 | } 32 | 33 | // Repository is a code repository 34 | type Repository struct { 35 | Name string 36 | Owner struct { 37 | Login string 38 | } 39 | NameWithOwner string 40 | URL string 41 | ForkCount int64 42 | IsFork bool 43 | IsMirror bool 44 | IsPrivate bool 45 | CreatedAt githubv4.DateTime 46 | } 47 | 48 | // Repositories is a list of GitHub repositories 49 | type Repositories []Repository 50 | 51 | // Frames converts the list of GitHub repositories to a Grafana Dataframe 52 | func (r Repositories) Frames() data.Frames { 53 | frame := data.NewFrame( 54 | "repositories", 55 | data.NewField("name", nil, []string{}), 56 | data.NewField("owner", nil, []string{}), 57 | data.NewField("name_with_owner", nil, []string{}), 58 | data.NewField("url", nil, []string{}), 59 | data.NewField("forks", nil, []int64{}), 60 | data.NewField("is_fork", nil, []bool{}), 61 | data.NewField("is_mirror", nil, []bool{}), 62 | data.NewField("is_private", nil, []bool{}), 63 | data.NewField("created_at", nil, []time.Time{}), 64 | ) 65 | 66 | for _, v := range r { 67 | frame.AppendRow( 68 | v.Name, 69 | v.Owner.Login, 70 | v.NameWithOwner, 71 | v.URL, 72 | v.ForkCount, 73 | v.IsFork, 74 | v.IsMirror, 75 | v.IsPrivate, 76 | v.CreatedAt.Time, 77 | ) 78 | } 79 | 80 | return data.Frames{frame} 81 | 82 | } 83 | 84 | // GetAllRepositories retrieves all available repositories for an organization 85 | func GetAllRepositories(ctx context.Context, client models.Client, opts models.ListRepositoriesOptions) (Repositories, error) { 86 | query := strings.Join([]string{ 87 | fmt.Sprintf("org:%s", opts.Owner), 88 | opts.Repository, 89 | }, " ") 90 | 91 | var ( 92 | variables = map[string]interface{}{ 93 | "cursor": (*githubv4.String)(nil), 94 | "query": githubv4.String(query), 95 | } 96 | 97 | repos = []Repository{} 98 | ) 99 | 100 | for { 101 | q := &QueryListRepositories{} 102 | if err := client.Query(ctx, q, variables); err != nil { 103 | return nil, err 104 | } 105 | r := make([]Repository, len(q.Search.Nodes)) 106 | 107 | for i, v := range q.Search.Nodes { 108 | r[i] = v.Repository 109 | } 110 | 111 | repos = append(repos, r...) 112 | 113 | if !q.Search.PageInfo.HasNextPage { 114 | break 115 | } 116 | variables["cursor"] = q.Search.PageInfo.EndCursor 117 | } 118 | 119 | return repos, nil 120 | } 121 | -------------------------------------------------------------------------------- /pkg/github/repositories_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleRepositoriesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.RepositoriesQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleRepositoriesQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleRepositories handles the plugin query for github tags 20 | func (s *QueryHandler) HandleRepositories(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleRepositoriesQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/repositories_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | func TestGetAllRepositories(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | opts = models.ListRepositoriesOptions{ 17 | Owner: "grafana", 18 | } 19 | ) 20 | 21 | testVariables := testutil.GetTestVariablesFunction("query", "cursor") 22 | 23 | client := testutil.NewTestClient(t, 24 | testVariables, 25 | testutil.GetTestQueryFunction(&QueryListRepositories{}), 26 | ) 27 | 28 | _, err := GetAllRepositories(ctx, client, opts) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | 34 | func TestRepositoriesDataFrame(t *testing.T) { 35 | createdAt, err := time.Parse(time.RFC3339, "2020-08-25T16:21:56+00:00") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | repositories := Repositories{ 41 | Repository{ 42 | Name: "grafana", 43 | Owner: struct{ Login string }{ 44 | Login: "grafana", 45 | }, 46 | NameWithOwner: "grafana/grafana", 47 | URL: "github.com/grafana/grafana", 48 | ForkCount: 10, 49 | IsFork: true, 50 | IsMirror: true, 51 | IsPrivate: false, 52 | CreatedAt: githubv4.DateTime{ 53 | Time: createdAt, 54 | }, 55 | }, 56 | Repository{ 57 | Name: "loki", 58 | Owner: struct{ Login string }{ 59 | Login: "grafana", 60 | }, 61 | NameWithOwner: "grafana/loki", 62 | URL: "github.com/grafana/loki", 63 | ForkCount: 12, 64 | IsFork: true, 65 | IsMirror: true, 66 | IsPrivate: false, 67 | CreatedAt: githubv4.DateTime{ 68 | Time: createdAt, 69 | }, 70 | }, 71 | } 72 | 73 | testutil.CheckGoldenFramer(t, "repositories", repositories) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/github/resource_handlers.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/grafana/github-datasource/pkg/httputil" 8 | "github.com/grafana/github-datasource/pkg/models" 9 | ) 10 | 11 | func handleGetLabels(ctx context.Context, client models.Client, r *http.Request) (Labels, error) { 12 | q := r.URL.Query() 13 | opts := models.ListLabelsOptions{ 14 | Repository: q.Get("repository"), 15 | Owner: q.Get("owner"), 16 | Query: q.Get("query"), 17 | } 18 | 19 | labels, err := GetAllLabels(ctx, client, opts) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return labels, nil 25 | } 26 | 27 | // HandleGetLabels is the HTTP handler for the resource call for getting GitHub labels 28 | func (d *Datasource) HandleGetLabels(w http.ResponseWriter, r *http.Request) { 29 | labels, err := handleGetLabels(r.Context(), d.client, r) 30 | if err != nil { 31 | httputil.WriteError(w, http.StatusBadRequest, err) 32 | return 33 | } 34 | 35 | httputil.WriteResponse(w, labels) 36 | } 37 | 38 | func handleGetMilestones(ctx context.Context, client models.Client, r *http.Request) (Milestones, error) { 39 | q := r.URL.Query() 40 | opts := models.ListMilestonesOptions{ 41 | Repository: q.Get("repository"), 42 | Owner: q.Get("owner"), 43 | Query: q.Get("query"), 44 | } 45 | 46 | milestones, err := GetAllMilestones(ctx, client, opts) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return milestones, nil 52 | } 53 | 54 | // HandleGetMilestones is the HTTP handler for the resource call for getting GitHub milestones 55 | func (d *Datasource) HandleGetMilestones(w http.ResponseWriter, r *http.Request) { 56 | milestones, err := handleGetMilestones(r.Context(), d.client, r) 57 | if err != nil { 58 | httputil.WriteError(w, http.StatusBadRequest, err) 59 | return 60 | } 61 | 62 | httputil.WriteResponse(w, milestones) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/github/stargazers_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleStargazersQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.StargazersQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleStargazersQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleStargazers handles the plugin query for GitHub stargazers 20 | func (s *QueryHandler) HandleStargazers(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleStargazersQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/stargazers_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/grafana/grafana-plugin-sdk-go/backend" 11 | "github.com/shurcooL/githubv4" 12 | ) 13 | 14 | func TestGetStargazers(t *testing.T) { 15 | var ( 16 | ctx = context.Background() 17 | opts = models.ListStargazersOptions{ 18 | Owner: "grafana", 19 | Repository: "grafana", 20 | } 21 | timeRange = backend.TimeRange{ 22 | From: time.Now().Add(-7 * 24 * time.Hour), 23 | To: time.Now(), 24 | } 25 | ) 26 | 27 | testVariables := testutil.GetTestVariablesFunction("name", "owner", "cursor") 28 | 29 | client := testutil.NewTestClient(t, 30 | testVariables, 31 | testutil.GetTestQueryFunction(&QueryStargazers{}), 32 | ) 33 | 34 | _, err := GetStargazers(ctx, client, opts, timeRange) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | } 39 | 40 | func TestStargazersDataframe(t *testing.T) { 41 | starredAt, err := time.Parse(time.RFC3339, "2023-01-14T10:25:41+00:00") 42 | 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | stargazers := StargazersWrapper{ 48 | StargazerWrapper{ 49 | Stargazer: Stargazer{ 50 | StarredAt: githubv4.DateTime{ 51 | Time: starredAt, 52 | }, 53 | Node: models.User{ 54 | ID: "NEVER", 55 | Login: "gonna", 56 | Name: "Give", 57 | Company: "You", 58 | Email: "up@example.org", 59 | }, 60 | }, 61 | StarCount: 1, 62 | }, 63 | StargazerWrapper{ 64 | Stargazer: Stargazer{ 65 | StarredAt: githubv4.DateTime{ 66 | Time: starredAt.Add(time.Minute * -2), 67 | }, 68 | Node: models.User{ 69 | ID: "NEVER", 70 | Login: "gonna", 71 | Name: "Let", 72 | Company: "You", 73 | Email: "down@example.org", 74 | }, 75 | }, 76 | StarCount: 2, 77 | }, 78 | StargazerWrapper{ 79 | Stargazer: Stargazer{ 80 | StarredAt: githubv4.DateTime{ 81 | Time: starredAt.Add(time.Minute * -4), 82 | }, 83 | Node: models.User{ 84 | ID: "NEVER", 85 | Login: "gonna", 86 | Name: "Run", 87 | Company: "Around", 88 | Email: "and_desert_you@example.org", 89 | URL: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", 90 | }, 91 | }, 92 | StarCount: 3, 93 | }, 94 | } 95 | 96 | testutil.CheckGoldenFramer(t, "stargazers", stargazers) 97 | } 98 | -------------------------------------------------------------------------------- /pkg/github/tags_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleTagsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.TagsQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleTagsQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleTags handles the plugin query for github tags 20 | func (s *QueryHandler) HandleTags(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleTagsQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/tags_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/grafana/github-datasource/pkg/models" 9 | "github.com/grafana/github-datasource/pkg/testutil" 10 | "github.com/shurcooL/githubv4" 11 | ) 12 | 13 | func TestGetAllTags(t *testing.T) { 14 | var ( 15 | ctx = context.Background() 16 | opts = models.ListTagsOptions{ 17 | Repository: "grafana", 18 | Owner: "grafana", 19 | } 20 | ) 21 | 22 | testVariables := testutil.GetTestVariablesFunction("name", "owner", "cursor") 23 | 24 | client := testutil.NewTestClient(t, 25 | testVariables, 26 | testutil.GetTestQueryFunction(&QueryListTags{}), 27 | ) 28 | 29 | _, err := GetAllTags(ctx, client, opts) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | } 34 | 35 | func TestListTags(t *testing.T) { 36 | var ( 37 | ctx = context.Background() 38 | opts = models.ListTagsOptions{ 39 | Repository: "grafana", 40 | Owner: "grafana", 41 | } 42 | from = time.Now().Add(-30 * 24 * time.Hour) 43 | to = time.Now() 44 | ) 45 | 46 | testVariables := testutil.GetTestVariablesFunction("name", "owner", "cursor") 47 | 48 | client := testutil.NewTestClient(t, 49 | testVariables, 50 | testutil.GetTestQueryFunction(&QueryListTags{}), 51 | ) 52 | 53 | _, err := GetTagsInRange(ctx, client, opts, from, to) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | } 58 | 59 | func TestTagsDataFrames(t *testing.T) { 60 | createdAt, err := time.Parse(time.RFC3339, "2020-08-25T16:21:56+00:00") 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | user := author{ 66 | Email: "first@example.com", 67 | User: user{ 68 | Login: "firstCommitter", 69 | Name: "First Committer", 70 | Company: "ACME Corp", 71 | }, 72 | } 73 | 74 | tags := Tags{ 75 | tagDTO{ 76 | Name: "v1.0.0", 77 | OID: "", 78 | Author: author{ 79 | Email: user.Email, 80 | Date: githubv4.GitTimestamp{ 81 | Time: createdAt, 82 | }, 83 | User: user.User, 84 | }, 85 | }, 86 | tagDTO{ 87 | Name: "v1.1.0", 88 | Author: author{ 89 | Email: user.Email, 90 | Date: githubv4.GitTimestamp{ 91 | Time: createdAt, 92 | }, 93 | User: user.User, 94 | }, 95 | OID: "", 96 | }, 97 | } 98 | 99 | testutil.CheckGoldenFramer(t, "tags", tags) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/github/vulnerabilites_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleVulnerabilitiesQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.VulnerabilityQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | return dfutil.FrameResponseWithError(s.Datasource.HandleVulnerabilitiesQuery(ctx, query, q)) 17 | } 18 | 19 | // HandleVulnerabilities handles the plugin query for github Vulnerabilities 20 | func (s *QueryHandler) HandleVulnerabilities(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 21 | return &backend.QueryDataResponse{ 22 | Responses: processQueries(ctx, req, s.handleVulnerabilitiesQuery), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/github/workflows_handler.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/github-datasource/pkg/dfutil" 7 | "github.com/grafana/github-datasource/pkg/models" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | ) 10 | 11 | func (s *QueryHandler) handleWorkflowsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 12 | query := &models.WorkflowsQuery{} 13 | if err := UnmarshalQuery(q.JSON, query); err != nil { 14 | return *err 15 | } 16 | 17 | return dfutil.FrameResponseWithError(s.Datasource.HandleWorkflowsQuery(ctx, query, q)) 18 | } 19 | 20 | // HandleWorkflows handles the plugin query for GitHub workflows 21 | func (s *QueryHandler) HandleWorkflows(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 22 | return &backend.QueryDataResponse{ 23 | Responses: processQueries(ctx, req, s.handleWorkflowsQuery), 24 | }, nil 25 | } 26 | 27 | func (s *QueryHandler) handleWorkflowUsageQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 28 | query := &models.WorkflowUsageQuery{} 29 | if err := UnmarshalQuery(q.JSON, query); err != nil { 30 | return *err 31 | } 32 | 33 | return dfutil.FrameResponseWithError(s.Datasource.HandleWorkflowUsageQuery(ctx, query, q)) 34 | } 35 | 36 | // HandleWorkflowUsage handles the plugin query for GitHub workflows 37 | func (s *QueryHandler) HandleWorkflowUsage(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 38 | return &backend.QueryDataResponse{ 39 | Responses: processQueries(ctx, req, s.handleWorkflowUsageQuery), 40 | }, nil 41 | } 42 | 43 | func (s *QueryHandler) handleWorkflowRunsQuery(ctx context.Context, q backend.DataQuery) backend.DataResponse { 44 | query := &models.WorkflowRunsQuery{} 45 | if err := UnmarshalQuery(q.JSON, query); err != nil { 46 | return *err 47 | } 48 | 49 | return dfutil.FrameResponseWithError(s.Datasource.HandleWorkflowRunsQuery(ctx, query, q)) 50 | } 51 | 52 | // HandleWorkflowRuns handles the plugin query for GitHub workflows 53 | func (s *QueryHandler) HandleWorkflowRuns(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 54 | return &backend.QueryDataResponse{ 55 | Responses: processQueries(ctx, req, s.handleWorkflowRunsQuery), 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/httputil/errors.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // WriteError writes an error in JSON format to the ResponseWriter 12 | func WriteError(w http.ResponseWriter, statusCode int, err error) { 13 | d := map[string]string{ 14 | "error": err.Error(), 15 | } 16 | 17 | b, marshalError := json.Marshal(d) 18 | if marshalError != nil { 19 | w.WriteHeader(http.StatusInternalServerError) 20 | if _, err := w.Write([]byte(errors.Wrapf(marshalError, "error when marshalling error '%s'", err.Error()).Error())); err != nil { 21 | log.DefaultLogger.Error(err.Error()) 22 | } 23 | return 24 | } 25 | 26 | w.WriteHeader(statusCode) 27 | if _, err := w.Write(b); err != nil { 28 | log.DefaultLogger.Error(err.Error()) 29 | } 30 | } 31 | 32 | // WriteResponse writes a standard HTTP response to the ResponseWriter in JSON format 33 | func WriteResponse(w http.ResponseWriter, data interface{}) { 34 | b, err := json.Marshal(data) 35 | if err != nil { 36 | WriteError(w, http.StatusInternalServerError, err) 37 | return 38 | } 39 | w.Header().Add("Content-Type", "application/json") 40 | if _, err := w.Write(b); err != nil { 41 | log.DefaultLogger.Error(err.Error()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/backend" 7 | "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" 8 | 9 | "github.com/grafana/github-datasource/pkg/plugin" 10 | ) 11 | 12 | const dsID = "grafana-github-datasource" 13 | 14 | func main() { 15 | if err := datasource.Manage(dsID, plugin.NewDataSourceInstance, datasource.ManageOpts{}); err != nil { 16 | backend.Logger.Error(err.Error()) 17 | os.Exit(1) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/models/client.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | 6 | googlegithub "github.com/google/go-github/v72/github" 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | ) 9 | 10 | // The Client interface is satisfied by the githubv4.Client type. 11 | // Rather than accept the githubv4.Client type everywhere, we will follow the Go idiom of accepting interfaces / returning structs and accept this interface. 12 | type Client interface { 13 | Query(ctx context.Context, q interface{}, variables map[string]interface{}) error 14 | ListWorkflows(ctx context.Context, owner, repo string, opts *googlegithub.ListOptions) (*googlegithub.Workflows, *googlegithub.Response, error) 15 | GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (WorkflowUsage, error) 16 | GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error) 17 | ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) 18 | ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/models/codescanning.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type CodeScanningOptions struct { 4 | // Owner is the owner of the repository (ex: grafana) 5 | Owner string `json:"owner"` 6 | 7 | // Repository is the name of the repository being queried (ex: grafana) 8 | Repository string `json:"repository"` 9 | 10 | // State is the state of the code scanning alerts. Can be one of: open, closed, dismissed, fixed. 11 | State string `json:"state"` 12 | 13 | // Ref is the Git reference for the results we want to list. 14 | // The ref for a branch can be formatted either as refs/heads/ or simply . 15 | // To reference a pull request use refs/pull//merge. 16 | Ref string `json:"gitRef"` 17 | } 18 | 19 | // CodeScanningOptionsWithRepo adds Owner and Repo to a CodeScanningOptions. This is just for convenience 20 | func CodeScanningOptionsWithRepo(opt CodeScanningOptions, owner string, repo string) CodeScanningOptions { 21 | return CodeScanningOptions{ 22 | Owner: owner, 23 | Repository: repo, 24 | Ref: opt.Ref, 25 | State: opt.State, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pkg/models/commits.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ListCommitsOptions provides options when retrieving commits 4 | type ListCommitsOptions struct { 5 | Repository string `json:"repository"` 6 | Owner string `json:"owner"` 7 | Ref string `json:"gitRef"` 8 | } 9 | 10 | // CommitsOptionsWithRepo adds Owner and Repo to a ListCommitsOptions. This is just for convenience 11 | func CommitsOptionsWithRepo(opt ListCommitsOptions, owner string, repo string) ListCommitsOptions { 12 | return ListCommitsOptions{ 13 | Owner: owner, 14 | Repository: repo, 15 | Ref: opt.Ref, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/models/contributors.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ListContributorsOptions are the available arguments when listing contributor 4 | type ListContributorsOptions struct { 5 | // Repository is the name of the repository being queried (ex: grafana) 6 | Repository string `json:"repository"` 7 | 8 | // Owner is the owner of the repository (ex: grafana) 9 | Owner string `json:"owner"` 10 | 11 | Query *string `json:"query,omitempty"` 12 | } 13 | 14 | // A User is a GitHub user 15 | type User struct { 16 | ID string 17 | Login string 18 | Name string 19 | Company string 20 | Email string 21 | URL string 22 | } 23 | -------------------------------------------------------------------------------- /pkg/models/docs.go: -------------------------------------------------------------------------------- 1 | // Package models contains data types that will be received in a request or data that will be sent in a response. 2 | // For example, this is a good place to store the format of the datasource settings. 3 | package models 4 | -------------------------------------------------------------------------------- /pkg/models/issues.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/shurcooL/githubv4" 4 | 5 | // IssueTimeField defines what time field to filter issues by (closed, opened...) 6 | type IssueTimeField uint32 7 | 8 | const ( 9 | // IssueCreatedAt is used when filtering when an Issue was opened 10 | IssueCreatedAt IssueTimeField = iota 11 | // IssueClosedAt is used when filtering when an Issue was closed 12 | IssueClosedAt 13 | // IssueUpdatedAt is used when filtering when an Issue was updated (last time) 14 | IssueUpdatedAt 15 | ) 16 | 17 | func (d IssueTimeField) String() string { 18 | return [...]string{"created", "closed", "updated"}[d] 19 | } 20 | 21 | // ListIssuesOptions provides options when retrieving issues 22 | type ListIssuesOptions struct { 23 | Repository string `json:"repository"` 24 | Owner string `json:"owner"` 25 | Filters *githubv4.IssueFilters `json:"filters"` 26 | Query *string `json:"query,omitempty"` 27 | TimeField IssueTimeField `json:"timeField"` 28 | } 29 | 30 | // IssueOptionsWithRepo adds the Owner and Repository values to a ListIssuesOptions. This is a convenience function because this is a common operation 31 | func IssueOptionsWithRepo(opt ListIssuesOptions, owner string, repo string) ListIssuesOptions { 32 | return ListIssuesOptions{ 33 | Owner: owner, 34 | Repository: repo, 35 | Filters: opt.Filters, 36 | Query: opt.Query, 37 | TimeField: opt.TimeField, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/models/labels.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ListLabelsOptions is provided when listing Labels in a repository 4 | type ListLabelsOptions struct { 5 | // Repository is the name of the repository being queried (ex: grafana) 6 | Repository string `json:"repository"` 7 | 8 | // Owner is the owner of the repository (ex: grafana) 9 | Owner string `json:"owner"` 10 | 11 | // Query searches labels by name and description 12 | Query string `json:"query"` 13 | } 14 | -------------------------------------------------------------------------------- /pkg/models/milestones.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/shurcooL/githubv4" 4 | 5 | // ListMilestonesOptions is provided when listing Labels in a repository 6 | type ListMilestonesOptions struct { 7 | // Repository is the name of the repository being queried (ex: grafana) 8 | Repository string `json:"repository"` 9 | 10 | // Owner is the owner of the repository (ex: grafana) 11 | Owner string `json:"owner"` 12 | 13 | // Query searches milestones by name and description 14 | Query string `json:"query"` 15 | } 16 | 17 | // Milestone is a GitHub Milestone 18 | type Milestone struct { 19 | Closed bool 20 | Creator struct { 21 | User User `graphql:"... on User"` 22 | } 23 | DueOn githubv4.DateTime 24 | ClosedAt githubv4.DateTime 25 | CreatedAt githubv4.DateTime 26 | State githubv4.MilestoneState 27 | Title string 28 | } 29 | -------------------------------------------------------------------------------- /pkg/models/packages.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | "github.com/shurcooL/githubv4" 9 | ) 10 | 11 | // ListPackagesOptions provides options when retrieving commits 12 | type ListPackagesOptions struct { 13 | Repository string `json:"repository"` 14 | Owner string `json:"owner"` 15 | Names string `json:"names"` 16 | PackageType githubv4.PackageType `json:"packageType"` 17 | } 18 | 19 | // PackagesOptionsWithRepo adds Owner and Repo to a ListPackagesOptions. This is just for convenience 20 | func PackagesOptionsWithRepo(opt ListPackagesOptions, owner string, repo string) (ListPackagesOptions, error) { 21 | err := validatePackageType(opt.PackageType) 22 | if err != nil { 23 | return ListPackagesOptions{}, err 24 | } 25 | 26 | return ListPackagesOptions{ 27 | Owner: owner, 28 | Repository: repo, 29 | Names: opt.Names, 30 | PackageType: opt.PackageType, 31 | }, nil 32 | } 33 | 34 | // validPackageTypes is a list of valid package types that are supported by the GitHub graphql API that we are using 35 | var validPackageTypes = []githubv4.PackageType{ 36 | githubv4.PackageTypeMaven, 37 | githubv4.PackageTypeDocker, 38 | githubv4.PackageTypeDebian, 39 | githubv4.PackageTypePypi, 40 | } 41 | 42 | // notSupportedPackageTypes is a list of package types that are not supported by the GitHub graphql API 43 | // They were supported in the past but are not supported anymore and we want to return an error if they are used 44 | var notSupportedPackageTypes = []githubv4.PackageType{ 45 | githubv4.PackageTypeNpm, 46 | githubv4.PackageTypeRubygems, 47 | githubv4.PackageTypeNuget, 48 | } 49 | 50 | func validatePackageType(packageType githubv4.PackageType) error { 51 | if slices.Contains(validPackageTypes, packageType) { 52 | return nil 53 | } 54 | 55 | if slices.Contains(notSupportedPackageTypes, packageType) { 56 | return backend.DownstreamError(fmt.Errorf("package type %q is not supported. Valid types are: MAVEN, DOCKER, DEBIAN, PYPI", packageType)) 57 | } 58 | return backend.DownstreamError(fmt.Errorf("invalid package type %q. Valid types are: MAVEN, DOCKER, DEBIAN, PYPI", packageType)) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/models/packages_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/shurcooL/githubv4" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_validatePackageType(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | packageType githubv4.PackageType 14 | wantErr bool 15 | errMessage string 16 | }{ 17 | // Valid package types 18 | { 19 | name: "valid PYPI package type", 20 | packageType: githubv4.PackageTypePypi, 21 | wantErr: false, 22 | }, 23 | { 24 | name: "valid Docker package type", 25 | packageType: githubv4.PackageTypeDocker, 26 | wantErr: false, 27 | }, 28 | { 29 | name: "valid Maven package type", 30 | packageType: githubv4.PackageTypeMaven, 31 | wantErr: false, 32 | }, 33 | { 34 | name: "valid Debian package type", 35 | packageType: githubv4.PackageTypeDebian, 36 | wantErr: false, 37 | }, 38 | // Not supported package types by GraphQL API anymore 39 | { 40 | name: "not supported NPM package type", 41 | packageType: githubv4.PackageTypeNpm, 42 | wantErr: true, 43 | errMessage: `package type "NPM" is not supported`, 44 | }, 45 | { 46 | name: "not supported Rubygems package type", 47 | packageType: githubv4.PackageTypeRubygems, 48 | wantErr: true, 49 | errMessage: `package type "RUBYGEMS" is not supported`, 50 | }, 51 | { 52 | name: "not supported Nuget package type", 53 | packageType: githubv4.PackageTypeNuget, 54 | wantErr: true, 55 | errMessage: `package type "NUGET" is not supported`, 56 | }, 57 | // Invalid package types 58 | { 59 | name: "invalid package type", 60 | packageType: "INVALID", 61 | wantErr: true, 62 | errMessage: `invalid package type "INVALID"`, 63 | }, 64 | { 65 | name: "empty package type", 66 | packageType: "", 67 | wantErr: true, 68 | errMessage: `invalid package type ""`, 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | err := validatePackageType(tt.packageType) 75 | if tt.wantErr { 76 | require.Error(t, err) 77 | require.ErrorContains(t, err, tt.errMessage) 78 | } else { 79 | require.NoError(t, err) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/models/pagination.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/shurcooL/githubv4" 4 | 5 | // PageInfo is a GitHub type used in paginated responses 6 | type PageInfo struct { 7 | StartCursor githubv4.String 8 | EndCursor githubv4.String 9 | HasNextPage bool 10 | } 11 | -------------------------------------------------------------------------------- /pkg/models/projects.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ProjectsQuery is used when querying for GitHub Projects 4 | type ProjectsQuery struct { 5 | // Options ... 6 | Options ProjectOptions `json:"options"` 7 | } 8 | 9 | // ProjectQuery is used when querying for GitHub Project items 10 | type ProjectQuery struct { 11 | // Options ... 12 | Options ProjectOptions `json:"options"` 13 | } 14 | 15 | // ProjectOptions are the available options when listing project items 16 | type ProjectOptions struct { 17 | // Organization is the name of the organization being queried (ex: grafana) 18 | Organization string `json:"organization"` 19 | // Number is the project number 20 | Number any `json:"number"` 21 | // User is the name of the user who owns the project being queried 22 | User string `json:"user"` 23 | // Kind is the kind of query - Org vs User 24 | Kind int `json:"kind"` 25 | // Filters allow filtering the results 26 | Filters []Filter `json:"filters"` 27 | } 28 | 29 | // Filter allows filtering by Key/Value 30 | type Filter struct { 31 | // Key ... 32 | Key string 33 | // Value ... 34 | Value string 35 | // OP ... 36 | OP string 37 | // Conjunction ... 38 | Conjunction string 39 | } 40 | -------------------------------------------------------------------------------- /pkg/models/pull_requests.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // PullRequestTimeField defines what time field to filter pull requests by (closed, opened, merged...) 4 | type PullRequestTimeField uint32 5 | 6 | const ( 7 | // PullRequestClosedAt is used when filtering when a Pull Request was closed 8 | PullRequestClosedAt PullRequestTimeField = iota 9 | // PullRequestCreatedAt is used when filtering when a Pull Request was opened 10 | PullRequestCreatedAt 11 | // PullRequestMergedAt is used when filtering when a Pull Request was merged 12 | PullRequestMergedAt 13 | // PullRequestNone is used when the results are not filtered by time. Without any other filters, using this could easily cause an access token to be rate limited 14 | PullRequestNone 15 | ) 16 | 17 | func (d PullRequestTimeField) String() string { 18 | return [...]string{"closed", "created", "merged"}[d] 19 | } 20 | 21 | // ListPullRequestsOptions are the available options when listing pull requests in a time range 22 | type ListPullRequestsOptions struct { 23 | // Repository is the name of the repository being queried (ex: grafana) 24 | Repository string `json:"repository"` 25 | 26 | // Owner is the owner of the repository (ex: grafana) 27 | Owner string `json:"owner"` 28 | 29 | // TimeField defines what time field to filter by 30 | TimeField PullRequestTimeField `json:"timeField"` 31 | 32 | Query *string `json:"query,omitempty"` 33 | } 34 | 35 | // PullRequestOptionsWithRepo adds the Owner and Repository options to a ListPullRequestsOptions type 36 | func PullRequestOptionsWithRepo(opt ListPullRequestsOptions, owner string, repo string) ListPullRequestsOptions { 37 | return ListPullRequestsOptions{ 38 | Owner: owner, 39 | Repository: repo, 40 | Query: opt.Query, 41 | TimeField: opt.TimeField, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/models/releases.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ListReleasesOptions are the available options when listing releases 4 | type ListReleasesOptions struct { 5 | // Repository is the name of the repository being queried (ex: grafana) 6 | Repository string `json:"repository"` 7 | 8 | // Owner is the owner of the repository (ex: grafana) 9 | Owner string `json:"owner"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/models/repositories.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/shurcooL/githubv4" 4 | 5 | // ListRepositoriesOptions is the options for listing repositories 6 | type ListRepositoriesOptions struct { 7 | Owner string 8 | Repository string 9 | } 10 | 11 | // Repository is a code repository 12 | type Repository struct { 13 | Name string 14 | Owner struct { 15 | Login string 16 | } 17 | NameWithOwner string 18 | URL string 19 | ForkCount int64 20 | IsFork bool 21 | IsMirror bool 22 | IsPrivate bool 23 | CreatedAt githubv4.DateTime 24 | } 25 | -------------------------------------------------------------------------------- /pkg/models/settings.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/backend" 7 | ) 8 | 9 | type Settings struct { 10 | SelectedAuthType string `json:"selectedAuthType"` 11 | AccessToken string `json:"accessToken"` 12 | PrivateKey string `json:"privateKey"` 13 | AppId string `json:"appId"` 14 | InstallationId string `json:"installationId"` 15 | GitHubURL string `json:"githubUrl"` 16 | CachingEnabled bool `json:"cachingEnabled"` 17 | } 18 | 19 | func LoadSettings(settings backend.DataSourceInstanceSettings) (Settings, error) { 20 | s := Settings{} 21 | if err := json.Unmarshal(settings.JSONData, &s); err != nil { 22 | return Settings{}, err 23 | } 24 | 25 | if val, ok := settings.DecryptedSecureJSONData["accessToken"]; ok { 26 | s.AccessToken = val 27 | } 28 | 29 | if val, ok := settings.DecryptedSecureJSONData["privateKey"]; ok { 30 | s.PrivateKey = val 31 | } 32 | 33 | // Data sources created before the auth type was introduced will have an accessToken but no auth type. 34 | // In this case, we default to personal access token. 35 | if s.AccessToken != "" && s.SelectedAuthType == "" { 36 | s.SelectedAuthType = "personal-access-token" 37 | } 38 | 39 | return s, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/models/stargazers.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ListStargazersOptions is provided when fetching stargazers for a repository 4 | type ListStargazersOptions struct { 5 | // Owner is the owner of the repository (ex: grafana) 6 | Owner string `json:"owner"` 7 | 8 | // Repository is the name of the repository being queried (ex: grafana) 9 | Repository string `json:"repository"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/models/tags.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ListTagsOptions are the available options when listing tags 4 | type ListTagsOptions struct { 5 | // Repository is the name of the repository being queried (ex: grafana) 6 | Repository string `json:"repository"` 7 | 8 | // Owner is the owner of the repository (ex: grafana) 9 | Owner string `json:"owner"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/models/vulnerabilities.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // ListVulnerabilitiesOptions is provided when listing vulnerabilities in a repository 4 | type ListVulnerabilitiesOptions struct { 5 | // Repository is the name of the repository being queried (ex: grafana) 6 | Repository string `json:"repository"` 7 | 8 | // Owner is the owner of the repository (ex: grafana) 9 | Owner string `json:"owner"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/models/workflows.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | // WorkflowTimeField defines what time field to filter Workflows by. 6 | type WorkflowTimeField uint32 7 | 8 | const ( 9 | // WorkflowCreatedAt is used when filtering when an workflow was created 10 | WorkflowCreatedAt WorkflowTimeField = iota 11 | // WorkflowUpdatedAt is used when filtering when an Workflow was updated 12 | WorkflowUpdatedAt 13 | ) 14 | 15 | // ListWorkflowsOptions is provided when fetching workflows for a repository 16 | type ListWorkflowsOptions struct { 17 | // Owner is the owner of the repository (ex: grafana) 18 | Owner string `json:"owner"` 19 | 20 | // Repository is the name of the repository being queried (ex: grafana) 21 | Repository string `json:"repository"` 22 | 23 | // The field used to check if an entry is in the requested range. 24 | TimeField WorkflowTimeField `json:"timeField"` 25 | } 26 | 27 | // WorkflowUsageOptions is provided when fetching a specific workflow usage 28 | type WorkflowUsageOptions struct { 29 | // Owner is the owner of the repository (ex: grafana) 30 | Owner string `json:"owner"` 31 | 32 | // Repository is the name of the repository being queried (ex: grafana) 33 | Repository string `json:"repository"` 34 | 35 | // Workflow is the id or the workflow file name. 36 | Workflow string `json:"workflow"` 37 | 38 | // Branch is the branch to filter the runs by. 39 | Branch string `json:"branch"` 40 | } 41 | 42 | type WorkflowRunsOptions = WorkflowUsageOptions 43 | 44 | // WorkflowUsage contains a specific workflow usage information. 45 | type WorkflowUsage struct { 46 | CostUSD float64 47 | UniqueActors uint64 48 | Runs uint64 49 | SuccessfulRuns uint64 50 | FailedRuns uint64 51 | CancelledRuns uint64 52 | SkippedRuns uint64 53 | LongestRunDuration time.Duration 54 | TotalRunDuration time.Duration 55 | P95RunDuration time.Duration 56 | RunsPerWeekday map[time.Weekday]uint64 57 | UsagePerRunner map[string]time.Duration 58 | Name string 59 | } 60 | -------------------------------------------------------------------------------- /pkg/plugin/datasource.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/backend" 7 | 8 | "github.com/grafana/github-datasource/pkg/dfutil" 9 | "github.com/grafana/github-datasource/pkg/models" 10 | ) 11 | 12 | // The Datasource type handles the requests sent to the datasource backend 13 | type Datasource interface { 14 | HandleRepositoriesQuery(context.Context, *models.RepositoriesQuery, backend.DataQuery) (dfutil.Framer, error) 15 | HandleIssuesQuery(context.Context, *models.IssuesQuery, backend.DataQuery) (dfutil.Framer, error) 16 | HandleCommitsQuery(context.Context, *models.CommitsQuery, backend.DataQuery) (dfutil.Framer, error) 17 | HandleCodeScanningQuery(context.Context, *models.CodeScanningQuery, backend.DataQuery) (dfutil.Framer, error) 18 | HandleTagsQuery(context.Context, *models.TagsQuery, backend.DataQuery) (dfutil.Framer, error) 19 | HandleReleasesQuery(context.Context, *models.ReleasesQuery, backend.DataQuery) (dfutil.Framer, error) 20 | HandleContributorsQuery(context.Context, *models.ContributorsQuery, backend.DataQuery) (dfutil.Framer, error) 21 | HandlePullRequestsQuery(context.Context, *models.PullRequestsQuery, backend.DataQuery) (dfutil.Framer, error) 22 | HandleLabelsQuery(context.Context, *models.LabelsQuery, backend.DataQuery) (dfutil.Framer, error) 23 | HandlePackagesQuery(context.Context, *models.PackagesQuery, backend.DataQuery) (dfutil.Framer, error) 24 | HandleMilestonesQuery(context.Context, *models.MilestonesQuery, backend.DataQuery) (dfutil.Framer, error) 25 | HandleVulnerabilitiesQuery(context.Context, *models.VulnerabilityQuery, backend.DataQuery) (dfutil.Framer, error) 26 | HandleProjectsQuery(context.Context, *models.ProjectsQuery, backend.DataQuery) (dfutil.Framer, error) 27 | HandleStargazersQuery(context.Context, *models.StargazersQuery, backend.DataQuery) (dfutil.Framer, error) 28 | HandleWorkflowsQuery(context.Context, *models.WorkflowsQuery, backend.DataQuery) (dfutil.Framer, error) 29 | HandleWorkflowUsageQuery(context.Context, *models.WorkflowUsageQuery, backend.DataQuery) (dfutil.Framer, error) 30 | HandleWorkflowRunsQuery(context.Context, *models.WorkflowRunsQuery, backend.DataQuery) (dfutil.Framer, error) 31 | CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) 32 | QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/plugin/datasource_caching_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | "github.com/grafana/grafana-plugin-sdk-go/data" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // mockFramer is a struct implementing the Framer interface that returns predefined frames for testing purposes 14 | type mockFramer struct { 15 | frames data.Frames 16 | } 17 | 18 | func (m mockFramer) Frames() data.Frames { 19 | return m.frames 20 | } 21 | 22 | // Fixture for the test cases 23 | var dataQueryA = backend.DataQuery{JSON: json.RawMessage(`{"query": "A"}`)} 24 | var framesA = data.Frames{data.NewFrame("A", nil)} 25 | var dataQueryB = backend.DataQuery{JSON: json.RawMessage(`{"query": "B"}`)} 26 | var framesB = data.Frames{data.NewFrame("B", nil)} 27 | 28 | func TestWithCaching(t *testing.T) { 29 | cachedDS := WithCaching(nil) 30 | 31 | t.Run("read from empty cache concurrently", func(t *testing.T) { 32 | var wg sync.WaitGroup 33 | 34 | // Read goroutine 1 35 | wg.Add(1) 36 | go func() { 37 | defer wg.Done() 38 | 39 | f, err := cachedDS.getCache(dataQueryA) 40 | assert.Nil(t, f) 41 | assert.ErrorIs(t, err, ErrNoValue) 42 | }() 43 | 44 | // Read goroutine 2 45 | wg.Add(1) 46 | go func() { 47 | defer wg.Done() 48 | 49 | f, err := cachedDS.getCache(dataQueryA) 50 | assert.Nil(t, f) 51 | assert.ErrorIs(t, err, ErrNoValue) 52 | }() 53 | 54 | wg.Wait() 55 | }) 56 | 57 | t.Run("write to and read from cache concurrently", func(t *testing.T) { 58 | var wg sync.WaitGroup 59 | 60 | // Write goroutine 1 61 | wg.Add(1) 62 | go func() { 63 | defer wg.Done() 64 | 65 | f, err := cachedDS.saveCache(dataQueryA, mockFramer{frames: framesA}, nil) 66 | assert.NoError(t, err) 67 | assert.Equal(t, framesA, f.Frames()) 68 | }() 69 | 70 | // Write goroutine 2 71 | wg.Add(1) 72 | go func() { 73 | defer wg.Done() 74 | 75 | f, err := cachedDS.saveCache(dataQueryB, mockFramer{frames: framesB}, nil) 76 | assert.NoError(t, err) 77 | assert.Equal(t, framesB, f.Frames()) 78 | }() 79 | 80 | // Wait for writing goroutines 81 | wg.Wait() 82 | 83 | // Read goroutine 1 84 | wg.Add(1) 85 | go func() { 86 | defer wg.Done() 87 | 88 | f, err := cachedDS.getCache(dataQueryA) 89 | assert.NoError(t, err) 90 | assert.Equal(t, framesA, f.Frames()) 91 | }() 92 | 93 | // Read goroutine 2 94 | wg.Add(1) 95 | go func() { 96 | defer wg.Done() 97 | 98 | f, err := cachedDS.getCache(dataQueryB) 99 | assert.NoError(t, err) 100 | assert.Equal(t, framesB, f.Frames()) 101 | }() 102 | 103 | // Wait for reading goroutines 104 | wg.Wait() 105 | }) 106 | 107 | t.Run("read from the cache concurrently", func(t *testing.T) { 108 | var wg sync.WaitGroup 109 | 110 | // Read goroutine 1 111 | wg.Add(1) 112 | go func() { 113 | defer wg.Done() 114 | 115 | f, err := cachedDS.getCache(dataQueryA) 116 | assert.NoError(t, err) 117 | assert.Equal(t, framesA, f.Frames()) 118 | }() 119 | 120 | // Read goroutine 2 121 | wg.Add(1) 122 | go func() { 123 | defer wg.Done() 124 | 125 | f, err := cachedDS.getCache(dataQueryB) 126 | assert.NoError(t, err) 127 | assert.Equal(t, framesB, f.Frames()) 128 | }() 129 | 130 | // Wait for reading goroutines 131 | wg.Wait() 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /pkg/plugin/instance.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" 9 | 10 | "github.com/grafana/github-datasource/pkg/github" 11 | "github.com/grafana/github-datasource/pkg/models" 12 | ) 13 | 14 | // NewGitHubInstance creates a new GitHubInstance using the settings to determine if things like the Caching Wrapper should be enabled 15 | func NewGitHubInstance(ctx context.Context, settings models.Settings) (instancemgmt.Instance, error) { 16 | gh, err := github.NewDatasource(ctx, settings) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | var d Datasource = gh 22 | 23 | if settings.CachingEnabled { 24 | d = WithCaching(d) 25 | } 26 | 27 | return d, nil 28 | } 29 | 30 | // NewDataSourceInstance creates a new instance 31 | func NewDataSourceInstance(_ context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { 32 | datasourceSettings, err := models.LoadSettings(settings) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | datasourceSettings.CachingEnabled = true 38 | 39 | instance, err := NewGitHubInstance(context.Background(), datasourceSettings) 40 | if err != nil { 41 | return instance, fmt.Errorf("instantiating github instance: %w", err) 42 | } 43 | 44 | return instance, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/testutil/client.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | googlegithub "github.com/google/go-github/v72/github" 9 | "github.com/grafana/grafana-plugin-sdk-go/backend" 10 | 11 | "github.com/grafana/github-datasource/pkg/models" 12 | ) 13 | 14 | var ( 15 | // ErrTNil is returned by TestClient.Query(...) if the `testing.T` pointer in the TestClient is nil 16 | ErrTNil = errors.New("t is nil") 17 | ) 18 | 19 | // The TestClient satisfies the Client interface and implements the query function 20 | type TestClient struct { 21 | T *testing.T 22 | // TestVariables can be used 23 | TestVariables func(t *testing.T, variables map[string]interface{}) 24 | TestQuery func(t *testing.T, q interface{}) 25 | } 26 | 27 | // NewTestClient creates a new TestClient 28 | func NewTestClient(t *testing.T, 29 | testVariables func(t *testing.T, variables map[string]interface{}), 30 | testQuery func(t *testing.T, q interface{}), 31 | ) *TestClient { 32 | return &TestClient{ 33 | T: t, 34 | TestVariables: testVariables, 35 | TestQuery: testQuery, 36 | } 37 | } 38 | 39 | // Query calls the TestClient's caller-defined variables `TestVariables` and `TestQuery` 40 | func (c *TestClient) Query(ctx context.Context, q interface{}, variables map[string]interface{}) error { 41 | if c.T == nil { 42 | return ErrTNil 43 | } 44 | 45 | if c.TestVariables != nil { 46 | c.TestVariables(c.T, variables) 47 | } 48 | 49 | if c.TestQuery != nil { 50 | c.TestQuery(c.T, q) 51 | } 52 | return nil 53 | } 54 | 55 | // ListWorkflows is not implemented because it is not being used at the moment. 56 | func (c *TestClient) ListWorkflows(ctx context.Context, owner, repo string, opts *googlegithub.ListOptions) (*googlegithub.Workflows, *googlegithub.Response, error) { 57 | panic("unimplemented") 58 | } 59 | 60 | // GetWorkflowUsage is not implemented because it is not being used at the moment. 61 | func (c *TestClient) GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (models.WorkflowUsage, error) { 62 | panic("unimplemented") 63 | } 64 | 65 | // GetWorkflowRuns is not implemented because it is not being used at the moment. 66 | func (c *TestClient) GetWorkflowRuns(ctx context.Context, owner, repo, workflow string, branch string, timeRange backend.TimeRange) ([]*googlegithub.WorkflowRun, error) { 67 | panic("unimplemented") 68 | } 69 | 70 | // ListAlertsForRepo is not implemented because it is not being used in tests at the moment. 71 | func (c *TestClient) ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) { 72 | panic("unimplemented") 73 | } 74 | 75 | // ListAlertsForOrg is not implemented because it is not being used in tests at the moment. 76 | func (c *TestClient) ListAlertsForOrg(ctx context.Context, owner string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) { 77 | panic("unimplemented") 78 | } 79 | -------------------------------------------------------------------------------- /pkg/testutil/frames.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | "github.com/grafana/grafana-plugin-sdk-go/experimental" 10 | 11 | "github.com/grafana/github-datasource/pkg/dfutil" 12 | ) 13 | 14 | // UpdateGoldenFiles defines whether or not to update the files locally after checking the responses for validity 15 | const UpdateGoldenFiles bool = true 16 | 17 | // CheckGoldenFramer checks the GoldenDataResponse and creates a standard file format for saving them 18 | func CheckGoldenFramer(t *testing.T, name string, f dfutil.Framer) { 19 | dr := backend.DataResponse{ 20 | Frames: f.Frames(), 21 | } 22 | experimental.CheckGoldenJSONResponse(t, filepath.Join("testdata"), fmt.Sprintf("%s.golden", name), &dr, UpdateGoldenFiles) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/testutil/maputils.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import "testing" 4 | 5 | // EnsureKeyIsSet ensures that a single key is set in the map (m) 6 | func EnsureKeyIsSet(t *testing.T, m map[string]interface{}, key string) { 7 | if _, ok := m[key]; !ok { 8 | t.Errorf("key '%s' is not in map", key) 9 | } 10 | } 11 | 12 | // EnsureKeysAreSet ensures that all of the provided keys are set in the map (m) 13 | func EnsureKeysAreSet(t *testing.T, m map[string]interface{}, keys ...string) { 14 | for _, v := range keys { 15 | EnsureKeyIsSet(t, m, v) 16 | } 17 | } 18 | 19 | // GetTestVariablesFunction provides a function that satisfies the TestVariables function of a TestClient 20 | func GetTestVariablesFunction(keys ...string) func(*testing.T, map[string]interface{}) { 21 | return func(t *testing.T, m map[string]interface{}) { 22 | EnsureKeysAreSet(t, m, keys...) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/testutil/typeutils.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | // TypesAreEqual compares the types a and b. If they are not equal, then false is returned. If they are equal, then true is returned. 9 | func TypesAreEqual(a interface{}, b interface{}) bool { 10 | return reflect.TypeOf(a) == reflect.TypeOf(b) 11 | } 12 | 13 | // EnsureTypeEquality uses the test object and fails the test if the types are not equal 14 | func EnsureTypeEquality(t *testing.T, actual interface{}, expected interface{}) { 15 | if !TypesAreEqual(actual, expected) { 16 | t.Errorf("Types are not equal. Expected '%s', received '%s", reflect.TypeOf(actual).String(), reflect.TypeOf(expected).String()) 17 | } 18 | } 19 | 20 | // GetTestQueryFunction returns a function that satisfies the TestQuery function in the TestClient object 21 | func GetTestQueryFunction(expected interface{}) func(*testing.T, interface{}) { 22 | return func(t *testing.T, actual interface{}) { 23 | EnsureTypeEquality(t, actual, expected) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOptions } from '@grafana/plugin-e2e'; 2 | import { defineConfig, devices } from '@playwright/test'; 3 | import { dirname } from 'node:path'; 4 | 5 | const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`; 6 | 7 | /** 8 | * Read environment variables from file. 9 | * https://github.com/motdotla/dotenv 10 | */ 11 | // require('dotenv').config(); 12 | 13 | /** 14 | * See https://playwright.dev/docs/test-configuration. 15 | */ 16 | export default defineConfig({ 17 | testDir: './tests', 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: 'html', 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Base URL to use in actions like `await page.goto('/')`. */ 31 | baseURL: 'http://localhost:3000', 32 | 33 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 34 | trace: 'on-first-retry', 35 | }, 36 | 37 | /* Configure projects for major browsers */ 38 | projects: [ 39 | // 1. Login to Grafana and store the cookie on disk for use in other tests. 40 | { 41 | name: 'auth', 42 | testDir: pluginE2eAuth, 43 | testMatch: [/.*\.js/], 44 | }, 45 | // 2. Run tests in Google Chrome. Every test will start authenticated as admin user. 46 | { 47 | name: 'chromium', 48 | use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json' }, 49 | dependencies: ['auth'], 50 | }, 51 | ], 52 | }); 53 | -------------------------------------------------------------------------------- /scripts/debug-backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" == "-h" ]; then 3 | echo "Usage: ${BASH_SOURCE[0]} [plugin process name] [port]" 4 | exit 5 | fi 6 | 7 | PORT="${2:-3222}" 8 | PLUGIN_NAME="${1:-gds_sheets}" 9 | 10 | if [ "$OSTYPE" == "linux-gnu" ]; then 11 | ptrace_scope=`cat /proc/sys/kernel/yama/ptrace_scope` 12 | if [ "$ptrace_scope" != 0 ]; then 13 | echo "WARNING: ptrace_scope set to value other than 0, this might prevent debugger from connecting, try writing \"0\" to /proc/sys/kernel/yama/ptrace_scope. 14 | Read more at https://www.kernel.org/doc/Documentation/security/Yama.txt" 15 | read -p "Set ptrace_scope to 0? y/N (default N)" set_ptrace_input 16 | if [ "$set_ptrace_input" == "y" ] || [ "$set_ptrace_input" == "Y" ]; then 17 | echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 18 | fi 19 | fi 20 | fi 21 | 22 | PLUGIN_PID=`pgrep ${PLUGIN_NAME}` 23 | ECHO "PLUGIN_PID: ${PLUGIN_PID}" 24 | dlv attach ${PLUGIN_PID} --headless --listen=:${PORT} --api-version 2 --log 25 | pkill dlv -------------------------------------------------------------------------------- /scripts/e2e.sh: -------------------------------------------------------------------------------- 1 | # runs e2e in a container 2 | 3 | docker stop plugin-e2e-test 4 | docker rm plugin-e2e-test 5 | 6 | cd ../ 7 | 8 | mage build:linux 9 | 10 | ip=$(ipconfig getifaddr en0) 11 | 12 | echo "$ip" 13 | 14 | rm -r ./provisioning 15 | cp -r ../plugin-provisioning/provisioning ./provisioning 16 | 17 | # build the build-pipeline inside the plugin e2e container and run "plugin e2etests" sub command 18 | docker run -it \ 19 | --add-host=localhost:"$ip" \ 20 | --add-host=127.0.0.1:"$ip" \ 21 | --network="host" \ 22 | -v $(pwd):/home \ 23 | --name=plugin-e2e-test grafana/grafana-plugin-ci-e2e:latest \ 24 | sh "/home/scripts/test.sh" -------------------------------------------------------------------------------- /scripts/restart-plugin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | # TODO: build arch should be parameterized 5 | mage build:linux 6 | docker exec google-sheets-datasource_grafana_1 pkill -f "/var/lib/grafana/plugins/google-sheets-datasource/dist/sheets-datasource_linux_amd64" 7 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | # runs inside the container so screenshots match 2 | echo "running" 3 | 4 | export GRAFANA_LICENSE_KEY=eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwczovL2dyYWZhbmEuY29tIiwic3ViIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwLyIsImp0aSI6IjgwNSIsIm5iZiI6MTU3ODU3NjUyNiwiaWF0IjoxNTc4NTc2NzgyLCJleHAiOjE2MTA5NzY0OTAsImxleHAiOjE2MTA5NzY0OTAsImxpZCI6IjEwNTAwIiwicHJvZCI6WyJncmFmYW5hLWVudGVycHJpc2UiXSwibWF4X3VzZXJzIjotMSwiY29tcGFueSI6IkdyYWZhbmEgTGFicyJ9.ZVEJSdj1FiQ15AlQvJzzDr-1IrT2cHmve9fSTkMYNo1vXBIdKAOFrggv3cYp1ev_FpRsJvGbCLXS0IsDIKushoEy_xd50vUSn_EO2gRZwDaZ-6ZCyWWmN4rMe1CgVI_oKEtQysjhwUDrOpDhRcl3BvjShoWxJzVL84SOnLw3VWuFKljS6fKlusx1C86Dw0Kq8RQQvi9xLyNl-_zVDNZy6UYNYSP-wstA4B1DxwwUWdcvBe1326aTD8aKbofSGY-16W6Ou_KSRDCQRSvLotsY6VkZCgXGVzgLqFQK7a74eEl2mMnkdRb8325NjrBh6uMVBVVCEoyGEBKvgHMqhyAc1qHGfpy8s9emd1UFVrF-RJmUjeUta_VT6vDAtPv1a7IPpkhrG2DsVnLivk_BOcwq3UuJP1ZxrUb_HkSvkqWAz7vT1bgk_i7noURfJz6GVDKviXLNAYm40QfMqIbSAClHiH1UmcYU53M1-Xy0bY2d40YPWH75f_ZVfTstaa9BoEK6YemCj9J2ezpBjuUFjGkTK9LFrnTb_MO3JZMrwNPl5Vu4hcGzPg80_6uFMymgt2SGKcPyErB3790DTcssSfWY01Uegt3PJT6IgE91fkcfM0TU_kukR3c_0OpTtuYm3MKaxrMv4UGV-sz6_jimi7HQOhVY-PgE6-0ZsU_vvmpOY-0 5 | 6 | cd ./home 7 | 8 | $(npm bin)/cypress install 9 | 10 | echo "starting tests" 11 | 12 | export CYPRESS_HOST=host.docker.internal 13 | export CYPRESS_BASE_URL=http://host.docker.internal:3000 14 | 15 | yarn e2e:update 16 | 17 | # this will simulate how it runs on ci 18 | # wget -O grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v0.5.40/grabpl 19 | # chmod +x grabpl 20 | # ./grabpl plugin e2etests -------------------------------------------------------------------------------- /src/DataSource.ts: -------------------------------------------------------------------------------- 1 | import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; 2 | import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; 3 | import { replaceVariables, GithubVariableSupport } from './variables'; 4 | import { isValid } from './validation'; 5 | import { prepareAnnotation } from 'migrations'; 6 | import { Observable, lastValueFrom } from 'rxjs'; 7 | import { trackRequest } from 'tracking'; 8 | import type { GitHubQuery } from './types/query'; 9 | import type { GitHubDataSourceOptions } from './types/config'; 10 | 11 | export class GitHubDataSource extends DataSourceWithBackend { 12 | templateSrv = getTemplateSrv(); 13 | 14 | constructor(instanceSettings: DataSourceInstanceSettings) { 15 | super(instanceSettings); 16 | this.annotations = { 17 | prepareAnnotation, 18 | }; 19 | this.variables = new GithubVariableSupport(this); 20 | } 21 | 22 | // Required by DataSourceApi. It executes queries based on the provided DataQueryRequest. 23 | query(request: DataQueryRequest): Observable { 24 | trackRequest(request); 25 | return super.query(request); 26 | } 27 | 28 | // Implemented as a part of DataSourceApi 29 | // Only execute queries that have a query type 30 | filterQuery = (query: GitHubQuery) => { 31 | return isValid(query) && !query.hide; 32 | }; 33 | 34 | // Implemented as a part of DataSourceApi. Interpolates variables and adds ad hoc filters to a list of GitHub queries. 35 | applyTemplateVariables(query: GitHubQuery, scoped: ScopedVars): GitHubQuery { 36 | return replaceVariables(this.templateSrv, query, scoped); 37 | } 38 | 39 | // Used in VariableQueryEditor to get the choices for variables 40 | async getChoices(query: GitHubQuery): Promise { 41 | const request = { 42 | targets: [ 43 | { 44 | ...query, 45 | refId: 'metricFindQuery', 46 | }, 47 | ], 48 | range: { 49 | to: {}, 50 | from: {}, 51 | }, 52 | } as DataQueryRequest; 53 | 54 | try { 55 | const res = await lastValueFrom(this.query(request)); 56 | const columns = (res?.data[0]?.fields || []).map((f: any) => f.name) || []; 57 | return columns; 58 | } catch (err) { 59 | return Promise.reject(err); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import { GrafanaTheme2 } from '@grafana/data'; 3 | import { useStyles2 } from '@grafana/ui'; 4 | import React from 'react'; 5 | 6 | // this custom component is necessary because the Grafana UI component is no backwards compatible with Grafana < 10.1.0 7 | export const Divider = () => { 8 | const styles = useStyles2(getStyles); 9 | return
; 10 | }; 11 | 12 | const getStyles = (theme: GrafanaTheme2) => { 13 | return { 14 | horizontalDivider: css({ 15 | borderTop: `1px solid ${theme.colors.border.weak}`, 16 | margin: theme.spacing(2, 0), 17 | width: '100%', 18 | }), 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/FieldSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InlineFieldRow, Select, Spinner } from '@grafana/ui'; 3 | import { css } from '@emotion/css'; 4 | 5 | interface Props { 6 | onChange: (value: string) => void; 7 | value?: string; 8 | options: string[]; 9 | width: number; 10 | loading: boolean; 11 | } 12 | 13 | const spannerCss = css` 14 | margin: 0px 3px; 15 | padding: 0px 3px; 16 | `; 17 | 18 | const FieldSelect = (props: Props) => { 19 | const { onChange, options, value, width, loading } = props; 20 | return ( 21 | 22 | setState(el.currentTarget.value)} 26 | onBlur={(el) => props.onChange({ ...props, gitRef, state: el.currentTarget.value })} 27 | /> 28 | 29 | 30 | setGitRef(el.currentTarget.value)} 35 | onBlur={(el) => props.onChange({ ...props, state, gitRef: el.currentTarget.value })} 36 | /> 37 | 38 | 39 | ); 40 | }; 41 | export default QueryEditorCodeScanning; 42 | -------------------------------------------------------------------------------- /src/views/QueryEditorCommits.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, InlineField } from '@grafana/ui'; 3 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 4 | import { components } from 'components/selectors'; 5 | import type { CommitsOptions } from '../types/query'; 6 | 7 | interface Props extends CommitsOptions { 8 | onChange: (value: CommitsOptions) => void; 9 | } 10 | 11 | const QueryEditorCommits = (props: Props) => { 12 | const [ref, setRef] = useState(props.gitRef || ''); 13 | return ( 14 | <> 15 | 16 | setRef(el.currentTarget.value)} 21 | onBlur={(el) => props.onChange({ ...props, gitRef: el.currentTarget.value })} 22 | /> 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default QueryEditorCommits; 29 | -------------------------------------------------------------------------------- /src/views/QueryEditorContributors.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, InlineField } from '@grafana/ui'; 3 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 4 | import type { ContributorsOptions } from '../types/query'; 5 | 6 | interface Props extends ContributorsOptions { 7 | onChange: (value: ContributorsOptions) => void; 8 | } 9 | 10 | const QueryEditorContributors = (props: Props) => { 11 | const [query, setQuery] = useState(props.query || ''); 12 | return ( 13 | <> 14 | 15 | setQuery(el.currentTarget.value)} 19 | onBlur={(el) => props.onChange({ ...props, query: el.currentTarget.value })} 20 | /> 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default QueryEditorContributors; 27 | -------------------------------------------------------------------------------- /src/views/QueryEditorIssues.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import QueryEditorIssues from './QueryEditorIssues'; 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import { components } from 'components/selectors'; 6 | 7 | describe('QueryEditorIssues', () => { 8 | it('should have CreatedAt, ClosedAt and UpdatedAt time field option', async () => { 9 | const props = { 10 | onChange: jest.fn(), 11 | }; 12 | render(); 13 | expect(screen.getByText('Time Field')).toBeInTheDocument(); 14 | userEvent.click(screen.getByTestId(components.QueryEditor.Issues.timeFieldInput)); 15 | expect(await screen.findByText('CreatedAt')).toBeInTheDocument(); 16 | expect(await screen.findByText('ClosedAt')).toBeInTheDocument(); 17 | expect(await screen.findByText('UpdatedAt')).toBeInTheDocument(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/views/QueryEditorIssues.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, Select, InlineField } from '@grafana/ui'; 3 | import { SelectableValue } from '@grafana/data'; 4 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 5 | import { components } from 'components/selectors'; 6 | import { IssueTimeField } from '../constants'; 7 | import type { IssuesOptions } from '../types/query'; 8 | 9 | interface Props extends IssuesOptions { 10 | onChange: (value: IssuesOptions) => void; 11 | } 12 | 13 | const timeFieldOptions: Array> = Object.keys(IssueTimeField) 14 | .filter((_, i) => IssueTimeField[i] !== undefined) 15 | .map((_, i) => { 16 | return { 17 | label: `${IssueTimeField[i]}`, 18 | value: i as IssueTimeField, 19 | }; 20 | }); 21 | 22 | const defaultTimeField = 0 as IssueTimeField; 23 | 24 | const QueryEditorIssues = (props: Props) => { 25 | const [query, setQuery] = useState(props.query || ''); 26 | return ( 27 | <> 28 | ( 32 | <> 33 | For more information, visit  34 | 39 | https://docs.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests 40 | 41 | 42 | )} 43 | interactive={true} 44 | > 45 | setQuery(el.currentTarget.value)} 49 | onBlur={(el) => 50 | props.onChange({ 51 | ...props, 52 | query: el.currentTarget.value, 53 | }) 54 | } 55 | /> 56 | 57 | 62 | setQuery(el.currentTarget.value)} 19 | onBlur={(el) => props.onChange({ ...props, query: el.currentTarget.value })} 20 | /> 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default QueryEditorLabels; 27 | -------------------------------------------------------------------------------- /src/views/QueryEditorMilestones.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, InlineField } from '@grafana/ui'; 3 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 4 | import type { MilestonesOptions } from '../types/query'; 5 | 6 | interface Props extends MilestonesOptions { 7 | onChange: (value: MilestonesOptions) => void; 8 | } 9 | 10 | const QueryEditorMilestones = (props: Props) => { 11 | const [query, setQuery] = useState(props.query || ''); 12 | return ( 13 | <> 14 | 15 | setQuery(el.currentTarget.value)} 19 | onBlur={(el) => 20 | props.onChange({ 21 | ...props, 22 | query: el.currentTarget.value, 23 | }) 24 | } 25 | /> 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default QueryEditorMilestones; 32 | -------------------------------------------------------------------------------- /src/views/QueryEditorPackages.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import QueryEditorIssues, { DefaultPackageType } from './QueryEditorPackages'; 3 | import { render } from '@testing-library/react'; 4 | import { PackageType } from './../constants'; 5 | 6 | describe('QueryEditorPackages', () => { 7 | it('should update package type to default one if no package is selected', async () => { 8 | const props = { 9 | onChange: jest.fn(), 10 | packageType: undefined, 11 | }; 12 | render(); 13 | expect(props.onChange).toHaveBeenCalledTimes(1); 14 | expect(props.onChange).toHaveBeenCalledWith({ packageType: DefaultPackageType, onChange: props.onChange }); 15 | }); 16 | it('should not update package type to default one if package type is provided ', async () => { 17 | const props = { 18 | onChange: jest.fn(), 19 | packageType: PackageType.DOCKER, 20 | }; 21 | render(); 22 | expect(props.onChange).not.toHaveBeenCalled(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/views/QueryEditorPullRequests.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, Select, InlineField } from '@grafana/ui'; 3 | import { SelectableValue } from '@grafana/data'; 4 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 5 | import { PullRequestTimeField } from '../constants'; 6 | import type { PullRequestsOptions } from '../types/query'; 7 | 8 | interface Props extends PullRequestsOptions { 9 | onChange: (value: PullRequestsOptions) => void; 10 | } 11 | 12 | const timeFieldOptions: Array> = Object.keys(PullRequestTimeField) 13 | .filter((_, i) => PullRequestTimeField[i] !== undefined) 14 | .map((_, i) => { 15 | return { 16 | label: `${PullRequestTimeField[i]}`, 17 | value: i as PullRequestTimeField, 18 | }; 19 | }); 20 | 21 | const defaultTimeField = timeFieldOptions[0].value; 22 | 23 | const QueryEditorPullRequests = (props: Props) => { 24 | const [query, setQuery] = useState(props.query || ''); 25 | return ( 26 | <> 27 | ( 31 | <> 32 | For more information, visit  33 | 38 | https://docs.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests 39 | 40 | 41 | )} 42 | interactive={true} 43 | > 44 | setQuery(el.currentTarget.value)} 48 | onBlur={(el) => 49 | props.onChange({ 50 | ...props, 51 | query: el.currentTarget.value, 52 | }) 53 | } 54 | /> 55 | 56 | 61 | setOwner(el.currentTarget.value)} 37 | onBlur={(el) => 38 | props.onChange({ 39 | ...props, 40 | owner: el.currentTarget.value, 41 | }) 42 | } 43 | /> 44 | 45 | Repository 46 | 47 | setRepository(el.currentTarget.value)} 52 | onBlur={(el) => 53 | props.onChange({ 54 | ...props, 55 | repository: el.currentTarget.value, 56 | }) 57 | } 58 | /> 59 | 60 | ); 61 | }; 62 | 63 | export default QueryEditorRepositories; 64 | -------------------------------------------------------------------------------- /src/views/QueryEditorTags.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const QueryEditorTags = () => <>; 4 | export default QueryEditorTags; 5 | -------------------------------------------------------------------------------- /src/views/QueryEditorVulnerabilities.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const QueryEditorVulnerabilities = () => <>; 4 | export default QueryEditorVulnerabilities; 5 | -------------------------------------------------------------------------------- /src/views/QueryEditorWorkflowRuns.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, InlineField } from '@grafana/ui'; 3 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 4 | import type { WorkflowRunsOptions } from 'types/query'; 5 | 6 | interface Props extends WorkflowRunsOptions { 7 | onChange: (value: WorkflowRunsOptions) => void; 8 | } 9 | 10 | const QueryEditorWorkflowRuns = (props: Props) => { 11 | const [workflow, setWorkflow] = useState(props.workflow); 12 | const [branch, setBranch] = useState(props.branch); 13 | 14 | return ( 15 | <> 16 | 21 | setWorkflow(el.currentTarget.value)} 25 | onBlur={(el) => 26 | props.onChange({ 27 | ...props, 28 | workflow: el.currentTarget.value, 29 | }) 30 | } 31 | /> 32 | 33 | 38 | setBranch(el.currentTarget.value)} 42 | onBlur={(el) => 43 | props.onChange({ 44 | ...props, 45 | branch: el.currentTarget.value, 46 | }) 47 | } 48 | /> 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default QueryEditorWorkflowRuns; 55 | -------------------------------------------------------------------------------- /src/views/QueryEditorWorkflowUsage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Input, InlineField } from '@grafana/ui'; 3 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 4 | import type { WorkflowUsageOptions } from 'types/query'; 5 | 6 | interface Props extends WorkflowUsageOptions { 7 | onChange: (value: WorkflowUsageOptions) => void; 8 | } 9 | 10 | const QueryEditorWorkflowUsage = (props: Props) => { 11 | const [workflow, setWorkflow] = useState(props.workflow); 12 | 13 | return ( 14 | <> 15 | 20 | setWorkflow(el.currentTarget.value)} 24 | onBlur={(el) => 25 | props.onChange({ 26 | ...props, 27 | workflow: el.currentTarget.value, 28 | }) 29 | } 30 | /> 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default QueryEditorWorkflowUsage; 37 | -------------------------------------------------------------------------------- /src/views/QueryEditorWorkflows.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select, InlineField } from '@grafana/ui'; 3 | import { SelectableValue } from '@grafana/data'; 4 | import { RightColumnWidth, LeftColumnWidth } from './QueryEditor'; 5 | import { WorkflowsTimeField } from './../constants'; 6 | import type { WorkflowsOptions } from 'types/query'; 7 | 8 | interface Props extends WorkflowsOptions { 9 | onChange: (value: WorkflowsOptions) => void; 10 | } 11 | 12 | const timeFieldOptions: Array> = Object.keys(WorkflowsTimeField) 13 | .filter((_, i) => WorkflowsTimeField[i] !== undefined) 14 | .map((_, i) => { 15 | return { 16 | label: `${WorkflowsTimeField[i]}`, 17 | value: i as WorkflowsTimeField, 18 | }; 19 | }); 20 | 21 | const defaultTimeField = 0 as WorkflowsTimeField; 22 | 23 | const QueryEditorWorkflows = (props: Props) => { 24 | return ( 25 | <> 26 | 31 |