├── .editorconfig ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarnrc ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── catalog-info.yaml ├── dashboards ├── streaming.grafana ├── streaming.json ├── test-dashboard.grafana └── test-dashboard.json ├── devbox.json ├── devbox.lock ├── how-it-works.md ├── media ├── grafana_logo.png └── preview_commands.png ├── package.json ├── public ├── error.html ├── extensions-icon.png ├── grafana_icon.png ├── icon-theme.json ├── iframe.css └── webview.html ├── snippets.json ├── src ├── editor.ts ├── extension.ts ├── grafana.ts ├── middleware.ts ├── server.ts ├── telemetry.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts └── util.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | # 2 space indentation for JS stuff 13 | [*.{js,ts,tsx}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | # Tab indentation (no size specified) 18 | [Makefile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": "warn", 11 | "@typescript-eslint/semi": "warn", 12 | "curly": "warn", 13 | "eqeqeq": "warn", 14 | "no-throw-literal": "warn", 15 | "semi": "off" 16 | }, 17 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/platform-monitoring 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | github-actions: 9 | patterns: 10 | - "*" 11 | 12 | - package-ecosystem: "npm" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: ~ 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | linters: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 16 | with: 17 | persist-credentials: false 18 | 19 | - name: Install devbox 20 | uses: jetify-com/devbox-install-action@a03caf5813591bc882139eba6ae947930a83a427 #v0.11.0 21 | with: 22 | enable-cache: 'true' 23 | 24 | - name: Install vendors 25 | run: devbox run yarn install --frozen-lockfile 26 | 27 | - name: Compile codebase 28 | run: devbox run yarn run compile 29 | 30 | - name: Run linters 31 | run: devbox run yarn run lint 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "*" 5 | 6 | # These permissions are needed to assume roles from Github's OIDC. 7 | permissions: 8 | contents: write 9 | id-token: write 10 | 11 | name: Publish Extension 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 17 | with: 18 | persist-credentials: false 19 | 20 | - name: Install devbox 21 | uses: jetify-com/devbox-install-action@a03caf5813591bc882139eba6ae947930a83a427 #v0.11.0 22 | with: 23 | enable-cache: 'true' 24 | 25 | - name: Install vendors 26 | run: devbox run yarn install --frozen-lockfile 27 | 28 | - id: get-secrets 29 | uses: grafana/shared-workflows/actions/get-vault-secrets@75804962c1ba608148988c1e2dc35fbb0ee21746 30 | with: 31 | # Secrets placed in the ci/repo/grafana// path in Vault 32 | repo_secrets: | 33 | OPEN_VSX_TOKEN=openvsx:token 34 | VS_MARKETPLACE_TOKEN=vscode-marketplace:token 35 | 36 | - name: Create extension package 37 | run: devbox run vsce package --out grafana-vscode.vsix 38 | 39 | - name: Publish to Open VSX 40 | run: devbox run npx --yes ovsx@0.9.4 publish --pat ${{ env.OPEN_VSX_TOKEN }} --packagePath grafana-vscode.vsix 41 | 42 | - name: Publish to Visual Studio Marketplace 43 | run: devbox run vsce publish --pat ${{ env.VS_MARKETPLACE_TOKEN }} --packagePath grafana-vscode.vsix 44 | 45 | - name: Create GitHub release 46 | uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 #v1.15.0 47 | with: 48 | allowUpdates: true 49 | artifacts: "grafana-vscode.vsix" 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.vscode 2 | .yarn 3 | node_modules 4 | .idea 5 | dist 6 | *.vsix 7 | .vscode-test 8 | out 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 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 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}", 14 | "--disable-extensions" 15 | ], 16 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 17 | "preLaunchTask": "${defaultBuildTask}" 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "args": [ 24 | "--extensionDevelopmentPath=${workspaceFolder}", 25 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", 26 | "--disable-extensions" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/**/*.js", 30 | "${workspaceFolder}/dist/**/*.js" 31 | ], 32 | "preLaunchTask": "tasks: watch-tests" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | node_modules/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | webpack.config.js 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/.eslintrc.json 11 | **/*.map 12 | **/*.ts 13 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "grafana-vscode" extension will be documented in this file. 4 | 5 | ## v0.0.20 6 | - Add a walkthrough to help getting started with the extension (#118) 7 | 8 | ## v0.0.19 9 | - Add a "new dashboard" snippet (#96) 10 | - Support opening dashboards via the command palette (#97) 11 | 12 | ## v0.0.18 13 | - Fix support for Grafana Cloud (#87) 14 | - Support library panels (#90) 15 | - Support grizzly dashboards (#91) 16 | - Support HTTP API-style dashboards (#92) 17 | 18 | ## v0.0.17 19 | - Add /api/datasource/uid/* proxy endpoint 20 | - Remove preferences warning popup 21 | 22 | ## v0.0.16 23 | - Add additional proxy endpoints (to support more dashboards/etc) (#80) 24 | 25 | ## v0.0.15 26 | - Follow redirects and don't fail on trailing slashes (#77) 27 | - Theming (light/dark) support for Grafana (#74) 28 | 29 | ## v0.0.14 30 | - Improved readme (#62) 31 | 32 | ## v0.0.13 33 | - Add telemetry that will allow us to evaluate usefulness of this extension (#48) 34 | 35 | ## v0.0.12 36 | - Removed Kiosk mode - this was preventing the 'add panel' option from showing (#59) 37 | 38 | ## v0.0.11 39 | - Fixed usage on Windows (#57) 40 | 41 | ## v0.0.10 42 | - Readme tweaks (#54) 43 | 44 | ## v0.0.9 45 | - Added support for vscodium (#53) 46 | 47 | ## v0.0.8 48 | - Improved readme (#52) 49 | 50 | ## v0.0.7 51 | - First release to VSCode Marketplace 52 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Docs on CODEOWNERS: 2 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners 3 | # 4 | # Later codeowner matches take precedence over earlier ones. 5 | 6 | # Default owner 7 | * @grafana/platform-cat 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document is a guide to help you through the process of contributing to the `grafana-vs-code-extension`. 4 | 5 | ## Developing the extension 6 | 7 | ### Create a fork 8 | 9 | [Fork][fork], then clone the repository: 10 | 11 | ```shell 12 | git clone git@github.com:{your_github_username}/grafana-vs-code-extension.git 13 | cd grafana-vs-code-extension 14 | git remote add upstream https://github.com/grafana/grafana-vs-code-extension.git 15 | git fetch upstream 16 | ``` 17 | 18 | ### Install `devbox` 19 | 20 | This repository relies on [devbox](https://www.jetify.com/devbox/docs/) to manage all 21 | the tools and dependencies needed for its development. 22 | 23 | A shell including all the required tools is accessible via: 24 | 25 | ```shell 26 | devbox shell 27 | ``` 28 | 29 | This shell can be exited like any other shell, with `exit` or `CTRL+D`. 30 | 31 | One-off commands can be executed within the devbox shell as well: 32 | 33 | ```shell 34 | devbox run node --version 35 | ``` 36 | 37 | ### Install dependencies 38 | 39 | ```shell 40 | devbox run yarn install 41 | ``` 42 | 43 | ### Run the extension 44 | 45 | Compile the extension, and keep watching for changes: 46 | 47 | ```shell 48 | devbox run yarn watch 49 | ``` 50 | 51 | Open this repository in VS Code, then press `F5` to start the extension locally. 52 | 53 | ### Make changes 54 | 55 | To make changes to this codebase, follow the instructions on running the extension. Then, in your original VS Code window, make changes to the code base. 56 | 57 | Restart the extension with either CTRL + SHIFT + F5 (CMD + SHIFT + F5 on a Mac) or by clicking the green restart circle. 58 | 59 | Debug logs and dev tools can be accessed with CTRL + SHIFT + I on the VS Code development host. 60 | 61 | ## Releasing the extension 62 | 63 | Releasing is a two step process: 64 | 65 | 1. Prepare the release by updating the version number in `package.json` and describing what changed in the `CHANGELOG.md` file. See the [release preparation commit for `v0.0.19`](https://github.com/grafana/grafana-vs-code-extension/commit/71299f05d96391ce10b40bfe4de812955ace56dd). Open a pull request for this commit, get it reviewed and merged. 66 | 2. Trigger the release pipeline by creating and pushing a tag: `git tag {version} && git push origin {version}` 67 | 68 | > [!NOTE] 69 | > The release pipeline creates and publishes a `.vsix` artifact to Open VSX, the Visual Studio Marketplace, as well as a GitHub release. 70 | 71 | 72 | [fork]: https://github.com/grafana/grafana-vs-code-extension/fork 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Grafana Labs 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VS Code Extension for Grafana 2 | 3 | Grafana has an extensive UI for editing its dashboards. For many, this is sufficient for their needs. If this is you, this extension is not for you. 4 | 5 | However, some wish to use software development tools (e.g. git) to manage their dashboards and other observability resources. Dashboards can be exported as JSON, however this JSON is hard to understand and interpret. This extension allows you to: 6 | - Open a Grafana dashboard JSON file 7 | - Start a live preview of that dashboard inside VS Code, connected to live data from a Grafana instance of your choice 8 | - Edit the dashboard in the preview, using the normal Grafana dashboard editor UI 9 | - From the editor UI, save the updated dashboard back to the original JSON file 10 | 11 | Managing dashboards as code moves the single source of truth from Grafana itself to your version control system, which allows for dashboards to participate in gitops style workflows that are commonly used for much of the rest of software development. 12 | 13 | ## Why Work With Dashboards as Code? 14 | 15 | - JSON dashboards can be stored in your version control system. This provides a simple solution for rollback, history, auditing, and even change control. 16 | - If you have change-control policies for your Grafana stack, this extension allows you and other developers to test and verify dashboard changes before deploying them 17 | - Dashboards can be generated with tools like [Grafonnet](https://grafana.github.io/grafonnet/index.html) 18 | - Dashboards can be integrated into your IaC practices using [Terraform, Ansible, Grafana Operator, or Grizzly](https://grafana.com/blog/2022/12/06/a-complete-guide-to-managing-grafana-as-code-tools-tips-and-tricks/) 19 | 20 | ## Features 21 | 22 | - Reads a dashboard JSON you have locally. 23 | - Opens the dashboard configured in the JSON in a running Grafana instance, right inside your IDE. 24 | - Allows you to edit the dashboard from the UI. 25 | - Saves your changes to _your_ JSON when you hit "Save" in the preview. 26 | 27 | ## Requirements 28 | 29 | - Have a dashboard JSON handy. 30 | - Have a running instance of Grafana locally _or_ have access to a hosted Grafana instance. 31 | - If you intend to use a dashboard across multiple Grafana instances, you will need to use datasources that have been deployed via the API, as these datasources will require consistent UIDs. 32 | 33 | ## Usage: 34 | 35 | ### Install from the Marketplace 36 | 37 | 1. Select the Extensions icon (![extensions icon](./public/extensions-icon.png)) on the left bar in VSCode. 38 | 2. Enter `Grafana` into the search box. Select the option for `Grafana / Grafana Editor` and click `Install`. 39 | 3. Open the Settings tab inside the extension (CTRL+, (comma) or `cmd` + `,` on Mac) and search for `grafana`. Then select `Extensions`. 40 | 41 | ### Configure the Extension 42 | 43 | 1. Provide the default URL for your Grafana instance in the `URL` field. If you are using a local Grafana instance, the default value is `http://localhost:3000`. 44 | 2. Create a [Service account in Grafana](https://grafana.com/docs/grafana/latest/administration/service-accounts/#create-a-service-account-in-grafana) and add a token to it. 45 | 3. In the VS Code settings, click `Set your token, securely` then paste your token into the popup. Press ENTER. 46 | 47 | ### Using the Extension 48 | 49 | 1. Open a folder on your computer that has some dashboard JSON (if you don't have any of your own, navigate to the `dashboards` folder of [this repo](https://github.com/grafana/grafana-vs-code-extension/tree/main/dashboards)). 50 | 2. Right-click on a dashboard JSON file in the file explorer and select `Edit in Grafana`. 51 | 3. Have fun! 52 | 4. Note, clicking `save` on your dashboard will update the JSON file in your local folder. 53 | 54 | ## Extension Settings 55 | 56 | - `grafana-vscode.URL`: Set the URL of the Grafana instance you want to open the dashboard in. Defaults to 'http://localhost:3000'. 57 | - `grafana-vscode.service-account-token`: A Service Account token. This is stored in the operating system secret store. Defaults to an empty string. 58 | 59 | ## Extension communication with Grafana 60 | Details of how this extension communicates with Grafana is available [here](https://github.com/grafana/grafana-vs-code-extension/blob/main/how-it-works.md). 61 | 62 | ## Contributing 63 | 64 | See our [contributing guide](CONTRIBUTING.md). 65 | 66 | ## Maturity 67 | 68 | The code in this repository should be considered as "**public preview**" and is actively developed and maintained by Engineering teams at Grafana. 69 | 70 | While this extension is stable enough to be used in production environments, occasional breaking changes or bugs can be expected. 71 | 72 | > [!NOTE] 73 | > Bugs and issues are handled solely by Engineering teams. On-call support or SLAs are not available. 74 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: grafana-vs-code-extension 5 | title: VS Code Extension for Grafana 6 | annotations: 7 | github.com/project-slug: grafana/grafana-vs-code-extension 8 | links: 9 | - title: Slack Channel 10 | url: https://raintank-corp.slack.com/archives/C018SLDD5MW 11 | spec: 12 | type: tool 13 | owner: group:default/platform-monitoring 14 | lifecycle: production 15 | -------------------------------------------------------------------------------- /dashboards/streaming.grafana: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "datasource", 8 | "uid": "grafana" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "id": 4194, 28 | "links": [], 29 | "liveNow": false, 30 | "panels": [ 31 | { 32 | "datasource": { 33 | "type": "datasource", 34 | "uid": "grafana" 35 | }, 36 | "fieldConfig": { 37 | "defaults": { 38 | "color": { 39 | "mode": "palette-classic" 40 | }, 41 | "custom": { 42 | "axisCenteredZero": false, 43 | "axisColorMode": "text", 44 | "axisLabel": "", 45 | "axisPlacement": "auto", 46 | "barAlignment": 0, 47 | "drawStyle": "line", 48 | "fillOpacity": 0, 49 | "gradientMode": "none", 50 | "hideFrom": { 51 | "legend": false, 52 | "tooltip": false, 53 | "viz": false 54 | }, 55 | "lineInterpolation": "linear", 56 | "lineWidth": 1, 57 | "pointSize": 5, 58 | "scaleDistribution": { 59 | "type": "linear" 60 | }, 61 | "showPoints": "auto", 62 | "spanNulls": false, 63 | "stacking": { 64 | "group": "A", 65 | "mode": "none" 66 | }, 67 | "thresholdsStyle": { 68 | "mode": "off" 69 | } 70 | }, 71 | "mappings": [], 72 | "thresholds": { 73 | "mode": "absolute", 74 | "steps": [ 75 | { 76 | "color": "green", 77 | "value": null 78 | }, 79 | { 80 | "color": "red", 81 | "value": 80 82 | } 83 | ] 84 | } 85 | }, 86 | "overrides": [] 87 | }, 88 | "gridPos": { 89 | "h": 10, 90 | "w": 12, 91 | "x": 0, 92 | "y": 0 93 | }, 94 | "id": 9, 95 | "options": { 96 | "legend": { 97 | "calcs": [], 98 | "displayMode": "list", 99 | "placement": "bottom", 100 | "showLegend": true 101 | }, 102 | "tooltip": { 103 | "mode": "single", 104 | "sort": "none" 105 | } 106 | }, 107 | "targets": [ 108 | { 109 | "channel": "plugin/testdata/random-20Hz-stream", 110 | "datasource": { 111 | "type": "datasource", 112 | "uid": "grafana" 113 | }, 114 | "queryType": "measurements", 115 | "refId": "A" 116 | } 117 | ], 118 | "title": "Streaming", 119 | "type": "timeseries" 120 | }, 121 | { 122 | "datasource": { 123 | "type": "testdata", 124 | "uid": "PD8C576611E62080A" 125 | }, 126 | "description": "Should be smaller given the longer value", 127 | "fieldConfig": { 128 | "defaults": { 129 | "color": { 130 | "mode": "palette-classic" 131 | }, 132 | "custom": { 133 | "axisCenteredZero": false, 134 | "axisColorMode": "text", 135 | "axisLabel": "", 136 | "axisPlacement": "auto", 137 | "axisSoftMin": 0, 138 | "fillOpacity": 80, 139 | "gradientMode": "none", 140 | "hideFrom": { 141 | "legend": false, 142 | "tooltip": false, 143 | "viz": false 144 | }, 145 | "lineWidth": 0, 146 | "scaleDistribution": { 147 | "type": "linear" 148 | }, 149 | "thresholdsStyle": { 150 | "mode": "off" 151 | } 152 | }, 153 | "decimals": 2, 154 | "mappings": [], 155 | "thresholds": { 156 | "mode": "absolute", 157 | "steps": [ 158 | { 159 | "color": "green", 160 | "value": null 161 | }, 162 | { 163 | "color": "red", 164 | "value": 80 165 | } 166 | ] 167 | } 168 | }, 169 | "overrides": [] 170 | }, 171 | "gridPos": { 172 | "h": 10, 173 | "w": 12, 174 | "x": 12, 175 | "y": 0 176 | }, 177 | "id": 15, 178 | "options": { 179 | "barRadius": 0, 180 | "barWidth": 1, 181 | "fullHighlight": false, 182 | "groupWidth": 0.82, 183 | "legend": { 184 | "calcs": [ 185 | "max" 186 | ], 187 | "displayMode": "list", 188 | "placement": "right", 189 | "showLegend": true 190 | }, 191 | "orientation": "auto", 192 | "showValue": "auto", 193 | "stacking": "none", 194 | "text": {}, 195 | "tooltip": { 196 | "mode": "single", 197 | "sort": "none" 198 | }, 199 | "xTickLabelRotation": 0, 200 | "xTickLabelSpacing": 0 201 | }, 202 | "targets": [ 203 | { 204 | "csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10", 205 | "datasource": { 206 | "type": "testdata", 207 | "uid": "PD8C576611E62080A" 208 | }, 209 | "refId": "A", 210 | "scenarioId": "csv_content" 211 | } 212 | ], 213 | "title": "Auto sizing & auto show values", 214 | "type": "barchart" 215 | }, 216 | { 217 | "datasource": { 218 | "type": "testdata", 219 | "uid": "PD8C576611E62080A" 220 | }, 221 | "fieldConfig": { 222 | "defaults": { 223 | "color": { 224 | "mode": "palette-classic" 225 | }, 226 | "custom": { 227 | "axisCenteredZero": false, 228 | "axisColorMode": "text", 229 | "axisLabel": "", 230 | "axisPlacement": "auto", 231 | "axisSoftMin": 0, 232 | "fillOpacity": 80, 233 | "gradientMode": "none", 234 | "hideFrom": { 235 | "legend": false, 236 | "tooltip": false, 237 | "viz": false 238 | }, 239 | "lineWidth": 0, 240 | "scaleDistribution": { 241 | "type": "linear" 242 | }, 243 | "thresholdsStyle": { 244 | "mode": "off" 245 | } 246 | }, 247 | "mappings": [], 248 | "thresholds": { 249 | "mode": "absolute", 250 | "steps": [ 251 | { 252 | "color": "green", 253 | "value": null 254 | }, 255 | { 256 | "color": "red", 257 | "value": 80 258 | } 259 | ] 260 | } 261 | }, 262 | "overrides": [] 263 | }, 264 | "gridPos": { 265 | "h": 11, 266 | "w": 8, 267 | "x": 0, 268 | "y": 10 269 | }, 270 | "id": 16, 271 | "options": { 272 | "barRadius": 0, 273 | "barWidth": 1, 274 | "fullHighlight": false, 275 | "groupWidth": 0.89, 276 | "legend": { 277 | "calcs": [ 278 | "max" 279 | ], 280 | "displayMode": "list", 281 | "placement": "right", 282 | "showLegend": true 283 | }, 284 | "orientation": "auto", 285 | "showValue": "auto", 286 | "stacking": "none", 287 | "text": {}, 288 | "tooltip": { 289 | "mode": "single", 290 | "sort": "none" 291 | }, 292 | "xTickLabelRotation": 0, 293 | "xTickLabelSpacing": 0 294 | }, 295 | "targets": [ 296 | { 297 | "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", 298 | "datasource": { 299 | "type": "testdata", 300 | "uid": "PD8C576611E62080A" 301 | }, 302 | "refId": "A", 303 | "scenarioId": "csv_content" 304 | } 305 | ], 306 | "title": "auto show values & No room for value", 307 | "type": "barchart" 308 | }, 309 | { 310 | "datasource": { 311 | "type": "testdata", 312 | "uid": "PD8C576611E62080A" 313 | }, 314 | "fieldConfig": { 315 | "defaults": { 316 | "color": { 317 | "mode": "palette-classic" 318 | }, 319 | "custom": { 320 | "axisCenteredZero": false, 321 | "axisColorMode": "text", 322 | "axisLabel": "", 323 | "axisPlacement": "auto", 324 | "axisSoftMin": 0, 325 | "fillOpacity": 80, 326 | "gradientMode": "none", 327 | "hideFrom": { 328 | "legend": false, 329 | "tooltip": false, 330 | "viz": false 331 | }, 332 | "lineWidth": 0, 333 | "scaleDistribution": { 334 | "type": "linear" 335 | }, 336 | "thresholdsStyle": { 337 | "mode": "off" 338 | } 339 | }, 340 | "mappings": [], 341 | "thresholds": { 342 | "mode": "absolute", 343 | "steps": [ 344 | { 345 | "color": "green", 346 | "value": null 347 | }, 348 | { 349 | "color": "red", 350 | "value": 80 351 | } 352 | ] 353 | } 354 | }, 355 | "overrides": [] 356 | }, 357 | "gridPos": { 358 | "h": 11, 359 | "w": 8, 360 | "x": 8, 361 | "y": 10 362 | }, 363 | "id": 17, 364 | "options": { 365 | "barRadius": 0, 366 | "barWidth": 1, 367 | "fullHighlight": false, 368 | "groupWidth": 0.89, 369 | "legend": { 370 | "calcs": [ 371 | "max" 372 | ], 373 | "displayMode": "list", 374 | "placement": "right", 375 | "showLegend": true 376 | }, 377 | "orientation": "auto", 378 | "showValue": "always", 379 | "stacking": "none", 380 | "text": { 381 | "valueSize": 100 382 | }, 383 | "tooltip": { 384 | "mode": "single", 385 | "sort": "none" 386 | }, 387 | "xTickLabelRotation": 0, 388 | "xTickLabelSpacing": 0 389 | }, 390 | "targets": [ 391 | { 392 | "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", 393 | "datasource": { 394 | "type": "testdata", 395 | "uid": "PD8C576611E62080A" 396 | }, 397 | "refId": "A", 398 | "scenarioId": "csv_content" 399 | } 400 | ], 401 | "title": "auto show values & Always show value", 402 | "type": "barchart" 403 | }, 404 | { 405 | "datasource": { 406 | "type": "datasource", 407 | "uid": "-- Dashboard --" 408 | }, 409 | "fieldConfig": { 410 | "defaults": { 411 | "color": { 412 | "mode": "palette-classic" 413 | }, 414 | "custom": { 415 | "axisCenteredZero": false, 416 | "axisColorMode": "text", 417 | "axisLabel": "", 418 | "axisPlacement": "auto", 419 | "barAlignment": 0, 420 | "drawStyle": "line", 421 | "fillOpacity": 0, 422 | "gradientMode": "none", 423 | "hideFrom": { 424 | "legend": false, 425 | "tooltip": false, 426 | "viz": false 427 | }, 428 | "lineInterpolation": "linear", 429 | "lineWidth": 1, 430 | "pointSize": 5, 431 | "scaleDistribution": { 432 | "type": "linear" 433 | }, 434 | "showPoints": "auto", 435 | "spanNulls": false, 436 | "stacking": { 437 | "group": "A", 438 | "mode": "none" 439 | }, 440 | "thresholdsStyle": { 441 | "mode": "off" 442 | } 443 | }, 444 | "mappings": [], 445 | "thresholds": { 446 | "mode": "absolute", 447 | "steps": [ 448 | { 449 | "color": "green", 450 | "value": null 451 | }, 452 | { 453 | "color": "red", 454 | "value": 80 455 | } 456 | ] 457 | } 458 | }, 459 | "overrides": [] 460 | }, 461 | "gridPos": { 462 | "h": 11, 463 | "w": 8, 464 | "x": 16, 465 | "y": 10 466 | }, 467 | "id": 10, 468 | "options": { 469 | "legend": { 470 | "calcs": [], 471 | "displayMode": "list", 472 | "placement": "bottom", 473 | "showLegend": true 474 | }, 475 | "tooltip": { 476 | "mode": "single", 477 | "sort": "none" 478 | } 479 | }, 480 | "targets": [ 481 | { 482 | "datasource": { 483 | "type": "datasource", 484 | "uid": "-- Dashboard --" 485 | }, 486 | "panelId": 9, 487 | "refId": "A" 488 | } 489 | ], 490 | "title": "Fixed value sizing", 491 | "type": "timeseries" 492 | }, 493 | { 494 | "datasource": { 495 | "type": "testdata", 496 | "uid": "PD8C576611E62080A" 497 | }, 498 | "fieldConfig": { 499 | "defaults": { 500 | "color": { 501 | "mode": "palette-classic" 502 | }, 503 | "custom": { 504 | "axisCenteredZero": false, 505 | "axisColorMode": "text", 506 | "axisLabel": "", 507 | "axisPlacement": "auto", 508 | "axisSoftMin": 0, 509 | "fillOpacity": 80, 510 | "gradientMode": "none", 511 | "hideFrom": { 512 | "legend": false, 513 | "tooltip": false, 514 | "viz": false 515 | }, 516 | "lineWidth": 0, 517 | "scaleDistribution": { 518 | "type": "linear" 519 | }, 520 | "thresholdsStyle": { 521 | "mode": "off" 522 | } 523 | }, 524 | "decimals": 7, 525 | "mappings": [], 526 | "thresholds": { 527 | "mode": "absolute", 528 | "steps": [ 529 | { 530 | "color": "green", 531 | "value": null 532 | }, 533 | { 534 | "color": "red", 535 | "value": 80 536 | } 537 | ] 538 | } 539 | }, 540 | "overrides": [] 541 | }, 542 | "gridPos": { 543 | "h": 11, 544 | "w": 12, 545 | "x": 0, 546 | "y": 21 547 | }, 548 | "id": 18, 549 | "options": { 550 | "barRadius": 0, 551 | "barWidth": 1, 552 | "fullHighlight": false, 553 | "groupWidth": 0.82, 554 | "legend": { 555 | "calcs": [ 556 | "max" 557 | ], 558 | "displayMode": "list", 559 | "placement": "right", 560 | "showLegend": true 561 | }, 562 | "orientation": "horizontal", 563 | "showValue": "auto", 564 | "stacking": "none", 565 | "text": {}, 566 | "tooltip": { 567 | "mode": "single", 568 | "sort": "none" 569 | }, 570 | "xTickLabelRotation": 0, 571 | "xTickLabelSpacing": 0 572 | }, 573 | "targets": [ 574 | { 575 | "csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, -5\nLondon, 10, 1\nLong value, 15,10", 576 | "datasource": { 577 | "type": "testdata", 578 | "uid": "PD8C576611E62080A" 579 | }, 580 | "refId": "A", 581 | "scenarioId": "csv_content" 582 | } 583 | ], 584 | "title": "Auto sizing & auto show values", 585 | "type": "barchart" 586 | }, 587 | { 588 | "datasource": { 589 | "type": "testdata", 590 | "uid": "PD8C576611E62080A" 591 | }, 592 | "description": "", 593 | "fieldConfig": { 594 | "defaults": { 595 | "color": { 596 | "mode": "palette-classic" 597 | }, 598 | "custom": { 599 | "axisCenteredZero": false, 600 | "axisColorMode": "text", 601 | "axisLabel": "", 602 | "axisPlacement": "auto", 603 | "axisSoftMin": 0, 604 | "fillOpacity": 80, 605 | "gradientMode": "none", 606 | "hideFrom": { 607 | "legend": false, 608 | "tooltip": false, 609 | "viz": false 610 | }, 611 | "lineWidth": 0, 612 | "scaleDistribution": { 613 | "type": "linear" 614 | }, 615 | "thresholdsStyle": { 616 | "mode": "off" 617 | } 618 | }, 619 | "mappings": [], 620 | "thresholds": { 621 | "mode": "absolute", 622 | "steps": [ 623 | { 624 | "color": "green", 625 | "value": null 626 | }, 627 | { 628 | "color": "red", 629 | "value": 80 630 | } 631 | ] 632 | } 633 | }, 634 | "overrides": [] 635 | }, 636 | "gridPos": { 637 | "h": 11, 638 | "w": 12, 639 | "x": 12, 640 | "y": 21 641 | }, 642 | "id": 19, 643 | "options": { 644 | "barRadius": 0, 645 | "barWidth": 1, 646 | "fullHighlight": false, 647 | "groupWidth": 0.89, 648 | "legend": { 649 | "calcs": [ 650 | "max" 651 | ], 652 | "displayMode": "list", 653 | "placement": "right", 654 | "showLegend": true 655 | }, 656 | "orientation": "horizontal", 657 | "showValue": "auto", 658 | "stacking": "none", 659 | "text": {}, 660 | "tooltip": { 661 | "mode": "single", 662 | "sort": "none" 663 | }, 664 | "xTickLabelRotation": 0, 665 | "xTickLabelSpacing": 0 666 | }, 667 | "targets": [ 668 | { 669 | "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", 670 | "datasource": { 671 | "type": "testdata", 672 | "uid": "PD8C576611E62080A" 673 | }, 674 | "refId": "A", 675 | "scenarioId": "csv_content" 676 | } 677 | ], 678 | "title": "auto show values & little room", 679 | "type": "barchart" 680 | } 681 | ], 682 | "refresh": "", 683 | "schemaVersion": 38, 684 | "style": "dark", 685 | "tags": [ 686 | "gdev", 687 | "panel-tests", 688 | "barchart" 689 | ], 690 | "templating": { 691 | "list": [] 692 | }, 693 | "time": { 694 | "from": "now-15m", 695 | "to": "now" 696 | }, 697 | "timepicker": {}, 698 | "timezone": "", 699 | "title": "BarChart - Panel Tests - Value sizing Copy", 700 | "uid": "wziLqrvnz", 701 | "version": 5, 702 | "weekStart": "" 703 | } -------------------------------------------------------------------------------- /dashboards/streaming.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "datasource", 8 | "uid": "grafana" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "id": 4194, 28 | "links": [], 29 | "liveNow": false, 30 | "panels": [ 31 | { 32 | "datasource": { 33 | "type": "datasource", 34 | "uid": "grafana" 35 | }, 36 | "fieldConfig": { 37 | "defaults": { 38 | "color": { 39 | "mode": "palette-classic" 40 | }, 41 | "custom": { 42 | "axisCenteredZero": false, 43 | "axisColorMode": "text", 44 | "axisLabel": "", 45 | "axisPlacement": "auto", 46 | "barAlignment": 0, 47 | "drawStyle": "line", 48 | "fillOpacity": 0, 49 | "gradientMode": "none", 50 | "hideFrom": { 51 | "legend": false, 52 | "tooltip": false, 53 | "viz": false 54 | }, 55 | "lineInterpolation": "linear", 56 | "lineWidth": 1, 57 | "pointSize": 5, 58 | "scaleDistribution": { 59 | "type": "linear" 60 | }, 61 | "showPoints": "auto", 62 | "spanNulls": false, 63 | "stacking": { 64 | "group": "A", 65 | "mode": "none" 66 | }, 67 | "thresholdsStyle": { 68 | "mode": "off" 69 | } 70 | }, 71 | "mappings": [], 72 | "thresholds": { 73 | "mode": "absolute", 74 | "steps": [ 75 | { 76 | "color": "green", 77 | "value": null 78 | }, 79 | { 80 | "color": "red", 81 | "value": 80 82 | } 83 | ] 84 | } 85 | }, 86 | "overrides": [] 87 | }, 88 | "gridPos": { 89 | "h": 10, 90 | "w": 12, 91 | "x": 0, 92 | "y": 0 93 | }, 94 | "id": 9, 95 | "options": { 96 | "legend": { 97 | "calcs": [], 98 | "displayMode": "list", 99 | "placement": "bottom", 100 | "showLegend": true 101 | }, 102 | "tooltip": { 103 | "mode": "single", 104 | "sort": "none" 105 | } 106 | }, 107 | "targets": [ 108 | { 109 | "channel": "plugin/testdata/random-20Hz-stream", 110 | "datasource": { 111 | "type": "datasource", 112 | "uid": "grafana" 113 | }, 114 | "queryType": "measurements", 115 | "refId": "A" 116 | } 117 | ], 118 | "title": "Streaming", 119 | "type": "timeseries" 120 | }, 121 | { 122 | "datasource": { 123 | "type": "testdata", 124 | "uid": "PD8C576611E62080A" 125 | }, 126 | "description": "Should be smaller given the longer value", 127 | "fieldConfig": { 128 | "defaults": { 129 | "color": { 130 | "mode": "palette-classic" 131 | }, 132 | "custom": { 133 | "axisCenteredZero": false, 134 | "axisColorMode": "text", 135 | "axisLabel": "", 136 | "axisPlacement": "auto", 137 | "axisSoftMin": 0, 138 | "fillOpacity": 80, 139 | "gradientMode": "none", 140 | "hideFrom": { 141 | "legend": false, 142 | "tooltip": false, 143 | "viz": false 144 | }, 145 | "lineWidth": 0, 146 | "scaleDistribution": { 147 | "type": "linear" 148 | }, 149 | "thresholdsStyle": { 150 | "mode": "off" 151 | } 152 | }, 153 | "decimals": 2, 154 | "mappings": [], 155 | "thresholds": { 156 | "mode": "absolute", 157 | "steps": [ 158 | { 159 | "color": "green", 160 | "value": null 161 | }, 162 | { 163 | "color": "red", 164 | "value": 80 165 | } 166 | ] 167 | } 168 | }, 169 | "overrides": [] 170 | }, 171 | "gridPos": { 172 | "h": 10, 173 | "w": 12, 174 | "x": 12, 175 | "y": 0 176 | }, 177 | "id": 15, 178 | "options": { 179 | "barRadius": 0, 180 | "barWidth": 1, 181 | "fullHighlight": false, 182 | "groupWidth": 0.82, 183 | "legend": { 184 | "calcs": [ 185 | "max" 186 | ], 187 | "displayMode": "list", 188 | "placement": "right", 189 | "showLegend": true 190 | }, 191 | "orientation": "auto", 192 | "showValue": "auto", 193 | "stacking": "none", 194 | "text": {}, 195 | "tooltip": { 196 | "mode": "single", 197 | "sort": "none" 198 | }, 199 | "xTickLabelRotation": 0, 200 | "xTickLabelSpacing": 0 201 | }, 202 | "targets": [ 203 | { 204 | "csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10", 205 | "datasource": { 206 | "type": "testdata", 207 | "uid": "PD8C576611E62080A" 208 | }, 209 | "refId": "A", 210 | "scenarioId": "csv_content" 211 | } 212 | ], 213 | "title": "Auto sizing & auto show values", 214 | "type": "barchart" 215 | }, 216 | { 217 | "datasource": { 218 | "type": "testdata", 219 | "uid": "PD8C576611E62080A" 220 | }, 221 | "fieldConfig": { 222 | "defaults": { 223 | "color": { 224 | "mode": "palette-classic" 225 | }, 226 | "custom": { 227 | "axisCenteredZero": false, 228 | "axisColorMode": "text", 229 | "axisLabel": "", 230 | "axisPlacement": "auto", 231 | "axisSoftMin": 0, 232 | "fillOpacity": 80, 233 | "gradientMode": "none", 234 | "hideFrom": { 235 | "legend": false, 236 | "tooltip": false, 237 | "viz": false 238 | }, 239 | "lineWidth": 0, 240 | "scaleDistribution": { 241 | "type": "linear" 242 | }, 243 | "thresholdsStyle": { 244 | "mode": "off" 245 | } 246 | }, 247 | "mappings": [], 248 | "thresholds": { 249 | "mode": "absolute", 250 | "steps": [ 251 | { 252 | "color": "green", 253 | "value": null 254 | }, 255 | { 256 | "color": "red", 257 | "value": 80 258 | } 259 | ] 260 | } 261 | }, 262 | "overrides": [] 263 | }, 264 | "gridPos": { 265 | "h": 11, 266 | "w": 8, 267 | "x": 0, 268 | "y": 10 269 | }, 270 | "id": 16, 271 | "options": { 272 | "barRadius": 0, 273 | "barWidth": 1, 274 | "fullHighlight": false, 275 | "groupWidth": 0.89, 276 | "legend": { 277 | "calcs": [ 278 | "max" 279 | ], 280 | "displayMode": "list", 281 | "placement": "right", 282 | "showLegend": true 283 | }, 284 | "orientation": "auto", 285 | "showValue": "auto", 286 | "stacking": "none", 287 | "text": {}, 288 | "tooltip": { 289 | "mode": "single", 290 | "sort": "none" 291 | }, 292 | "xTickLabelRotation": 0, 293 | "xTickLabelSpacing": 0 294 | }, 295 | "targets": [ 296 | { 297 | "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", 298 | "datasource": { 299 | "type": "testdata", 300 | "uid": "PD8C576611E62080A" 301 | }, 302 | "refId": "A", 303 | "scenarioId": "csv_content" 304 | } 305 | ], 306 | "title": "auto show values & No room for value", 307 | "type": "barchart" 308 | }, 309 | { 310 | "datasource": { 311 | "type": "testdata", 312 | "uid": "PD8C576611E62080A" 313 | }, 314 | "fieldConfig": { 315 | "defaults": { 316 | "color": { 317 | "mode": "palette-classic" 318 | }, 319 | "custom": { 320 | "axisCenteredZero": false, 321 | "axisColorMode": "text", 322 | "axisLabel": "", 323 | "axisPlacement": "auto", 324 | "axisSoftMin": 0, 325 | "fillOpacity": 80, 326 | "gradientMode": "none", 327 | "hideFrom": { 328 | "legend": false, 329 | "tooltip": false, 330 | "viz": false 331 | }, 332 | "lineWidth": 0, 333 | "scaleDistribution": { 334 | "type": "linear" 335 | }, 336 | "thresholdsStyle": { 337 | "mode": "off" 338 | } 339 | }, 340 | "mappings": [], 341 | "thresholds": { 342 | "mode": "absolute", 343 | "steps": [ 344 | { 345 | "color": "green", 346 | "value": null 347 | }, 348 | { 349 | "color": "red", 350 | "value": 80 351 | } 352 | ] 353 | } 354 | }, 355 | "overrides": [] 356 | }, 357 | "gridPos": { 358 | "h": 11, 359 | "w": 8, 360 | "x": 8, 361 | "y": 10 362 | }, 363 | "id": 17, 364 | "options": { 365 | "barRadius": 0, 366 | "barWidth": 1, 367 | "fullHighlight": false, 368 | "groupWidth": 0.89, 369 | "legend": { 370 | "calcs": [ 371 | "max" 372 | ], 373 | "displayMode": "list", 374 | "placement": "right", 375 | "showLegend": true 376 | }, 377 | "orientation": "auto", 378 | "showValue": "always", 379 | "stacking": "none", 380 | "text": { 381 | "valueSize": 100 382 | }, 383 | "tooltip": { 384 | "mode": "single", 385 | "sort": "none" 386 | }, 387 | "xTickLabelRotation": 0, 388 | "xTickLabelSpacing": 0 389 | }, 390 | "targets": [ 391 | { 392 | "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", 393 | "datasource": { 394 | "type": "testdata", 395 | "uid": "PD8C576611E62080A" 396 | }, 397 | "refId": "A", 398 | "scenarioId": "csv_content" 399 | } 400 | ], 401 | "title": "auto show values & Always show value", 402 | "type": "barchart" 403 | }, 404 | { 405 | "datasource": { 406 | "type": "datasource", 407 | "uid": "-- Dashboard --" 408 | }, 409 | "fieldConfig": { 410 | "defaults": { 411 | "color": { 412 | "mode": "palette-classic" 413 | }, 414 | "custom": { 415 | "axisCenteredZero": false, 416 | "axisColorMode": "text", 417 | "axisLabel": "", 418 | "axisPlacement": "auto", 419 | "barAlignment": 0, 420 | "drawStyle": "line", 421 | "fillOpacity": 0, 422 | "gradientMode": "none", 423 | "hideFrom": { 424 | "legend": false, 425 | "tooltip": false, 426 | "viz": false 427 | }, 428 | "lineInterpolation": "linear", 429 | "lineWidth": 1, 430 | "pointSize": 5, 431 | "scaleDistribution": { 432 | "type": "linear" 433 | }, 434 | "showPoints": "auto", 435 | "spanNulls": false, 436 | "stacking": { 437 | "group": "A", 438 | "mode": "none" 439 | }, 440 | "thresholdsStyle": { 441 | "mode": "off" 442 | } 443 | }, 444 | "mappings": [], 445 | "thresholds": { 446 | "mode": "absolute", 447 | "steps": [ 448 | { 449 | "color": "green", 450 | "value": null 451 | }, 452 | { 453 | "color": "red", 454 | "value": 80 455 | } 456 | ] 457 | } 458 | }, 459 | "overrides": [] 460 | }, 461 | "gridPos": { 462 | "h": 11, 463 | "w": 8, 464 | "x": 16, 465 | "y": 10 466 | }, 467 | "id": 10, 468 | "options": { 469 | "legend": { 470 | "calcs": [], 471 | "displayMode": "list", 472 | "placement": "bottom", 473 | "showLegend": true 474 | }, 475 | "tooltip": { 476 | "mode": "single", 477 | "sort": "none" 478 | } 479 | }, 480 | "targets": [ 481 | { 482 | "datasource": { 483 | "type": "datasource", 484 | "uid": "-- Dashboard --" 485 | }, 486 | "panelId": 9, 487 | "refId": "A" 488 | } 489 | ], 490 | "title": "Fixed value sizing", 491 | "type": "timeseries" 492 | }, 493 | { 494 | "datasource": { 495 | "type": "testdata", 496 | "uid": "PD8C576611E62080A" 497 | }, 498 | "fieldConfig": { 499 | "defaults": { 500 | "color": { 501 | "mode": "palette-classic" 502 | }, 503 | "custom": { 504 | "axisCenteredZero": false, 505 | "axisColorMode": "text", 506 | "axisLabel": "", 507 | "axisPlacement": "auto", 508 | "axisSoftMin": 0, 509 | "fillOpacity": 80, 510 | "gradientMode": "none", 511 | "hideFrom": { 512 | "legend": false, 513 | "tooltip": false, 514 | "viz": false 515 | }, 516 | "lineWidth": 0, 517 | "scaleDistribution": { 518 | "type": "linear" 519 | }, 520 | "thresholdsStyle": { 521 | "mode": "off" 522 | } 523 | }, 524 | "decimals": 7, 525 | "mappings": [], 526 | "thresholds": { 527 | "mode": "absolute", 528 | "steps": [ 529 | { 530 | "color": "green", 531 | "value": null 532 | }, 533 | { 534 | "color": "red", 535 | "value": 80 536 | } 537 | ] 538 | } 539 | }, 540 | "overrides": [] 541 | }, 542 | "gridPos": { 543 | "h": 11, 544 | "w": 12, 545 | "x": 0, 546 | "y": 21 547 | }, 548 | "id": 18, 549 | "options": { 550 | "barRadius": 0, 551 | "barWidth": 1, 552 | "fullHighlight": false, 553 | "groupWidth": 0.82, 554 | "legend": { 555 | "calcs": [ 556 | "max" 557 | ], 558 | "displayMode": "list", 559 | "placement": "right", 560 | "showLegend": true 561 | }, 562 | "orientation": "horizontal", 563 | "showValue": "auto", 564 | "stacking": "none", 565 | "text": {}, 566 | "tooltip": { 567 | "mode": "single", 568 | "sort": "none" 569 | }, 570 | "xTickLabelRotation": 0, 571 | "xTickLabelSpacing": 0 572 | }, 573 | "targets": [ 574 | { 575 | "csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, -5\nLondon, 10, 1\nLong value, 15,10", 576 | "datasource": { 577 | "type": "testdata", 578 | "uid": "PD8C576611E62080A" 579 | }, 580 | "refId": "A", 581 | "scenarioId": "csv_content" 582 | } 583 | ], 584 | "title": "Auto sizing & auto show values", 585 | "type": "barchart" 586 | }, 587 | { 588 | "datasource": { 589 | "type": "testdata", 590 | "uid": "PD8C576611E62080A" 591 | }, 592 | "description": "", 593 | "fieldConfig": { 594 | "defaults": { 595 | "color": { 596 | "mode": "palette-classic" 597 | }, 598 | "custom": { 599 | "axisCenteredZero": false, 600 | "axisColorMode": "text", 601 | "axisLabel": "", 602 | "axisPlacement": "auto", 603 | "axisSoftMin": 0, 604 | "fillOpacity": 80, 605 | "gradientMode": "none", 606 | "hideFrom": { 607 | "legend": false, 608 | "tooltip": false, 609 | "viz": false 610 | }, 611 | "lineWidth": 0, 612 | "scaleDistribution": { 613 | "type": "linear" 614 | }, 615 | "thresholdsStyle": { 616 | "mode": "off" 617 | } 618 | }, 619 | "mappings": [], 620 | "thresholds": { 621 | "mode": "absolute", 622 | "steps": [ 623 | { 624 | "color": "green", 625 | "value": null 626 | }, 627 | { 628 | "color": "red", 629 | "value": 80 630 | } 631 | ] 632 | } 633 | }, 634 | "overrides": [] 635 | }, 636 | "gridPos": { 637 | "h": 11, 638 | "w": 12, 639 | "x": 12, 640 | "y": 21 641 | }, 642 | "id": 19, 643 | "options": { 644 | "barRadius": 0, 645 | "barWidth": 1, 646 | "fullHighlight": false, 647 | "groupWidth": 0.89, 648 | "legend": { 649 | "calcs": [ 650 | "max" 651 | ], 652 | "displayMode": "list", 653 | "placement": "right", 654 | "showLegend": true 655 | }, 656 | "orientation": "horizontal", 657 | "showValue": "auto", 658 | "stacking": "none", 659 | "text": {}, 660 | "tooltip": { 661 | "mode": "single", 662 | "sort": "none" 663 | }, 664 | "xTickLabelRotation": 0, 665 | "xTickLabelSpacing": 0 666 | }, 667 | "targets": [ 668 | { 669 | "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", 670 | "datasource": { 671 | "type": "testdata", 672 | "uid": "PD8C576611E62080A" 673 | }, 674 | "refId": "A", 675 | "scenarioId": "csv_content" 676 | } 677 | ], 678 | "title": "auto show values & little room", 679 | "type": "barchart" 680 | } 681 | ], 682 | "refresh": "", 683 | "schemaVersion": 38, 684 | "style": "dark", 685 | "tags": [ 686 | "gdev", 687 | "panel-tests", 688 | "barchart" 689 | ], 690 | "templating": { 691 | "list": [] 692 | }, 693 | "time": { 694 | "from": "now-15m", 695 | "to": "now" 696 | }, 697 | "timepicker": {}, 698 | "timezone": "", 699 | "title": "BarChart - Panel Tests - Value sizing Copy", 700 | "uid": "wziLqrvnz", 701 | "version": 5, 702 | "weekStart": "" 703 | } -------------------------------------------------------------------------------- /dashboards/test-dashboard.grafana: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 4278, 22 | "links": [], 23 | "liveNow": false, 24 | "panels": [ 25 | { 26 | "datasource": { 27 | "type": "datasource", 28 | "uid": "grafana" 29 | }, 30 | "fieldConfig": { 31 | "defaults": { 32 | "color": { 33 | "mode": "palette-classic" 34 | }, 35 | "custom": { 36 | "axisCenteredZero": false, 37 | "axisColorMode": "text", 38 | "axisLabel": "", 39 | "axisPlacement": "auto", 40 | "fillOpacity": 80, 41 | "gradientMode": "none", 42 | "hideFrom": { 43 | "legend": false, 44 | "tooltip": false, 45 | "viz": false 46 | }, 47 | "lineWidth": 1, 48 | "scaleDistribution": { 49 | "type": "linear" 50 | }, 51 | "thresholdsStyle": { 52 | "mode": "off" 53 | } 54 | }, 55 | "mappings": [], 56 | "thresholds": { 57 | "mode": "absolute", 58 | "steps": [ 59 | { 60 | "color": "green", 61 | "value": null 62 | }, 63 | { 64 | "color": "red", 65 | "value": 80 66 | } 67 | ] 68 | } 69 | }, 70 | "overrides": [] 71 | }, 72 | "gridPos": { 73 | "h": 8, 74 | "w": 12, 75 | "x": 0, 76 | "y": 0 77 | }, 78 | "id": 1, 79 | "options": { 80 | "barRadius": 0, 81 | "barWidth": 0.97, 82 | "fullHighlight": false, 83 | "groupWidth": 0.7, 84 | "legend": { 85 | "calcs": [], 86 | "displayMode": "list", 87 | "placement": "bottom", 88 | "showLegend": true 89 | }, 90 | "orientation": "auto", 91 | "showValue": "auto", 92 | "stacking": "none", 93 | "tooltip": { 94 | "mode": "single", 95 | "sort": "none" 96 | }, 97 | "xTickLabelRotation": 0, 98 | "xTickLabelSpacing": 0 99 | }, 100 | "targets": [ 101 | { 102 | "channel": "plugin/testdata/random-2s-stream", 103 | "datasource": { 104 | "type": "datasource", 105 | "uid": "grafana" 106 | }, 107 | "filter": { 108 | "fields": [ 109 | "Time", 110 | "Min" 111 | ] 112 | }, 113 | "queryType": "randomWalk", 114 | "refId": "A" 115 | } 116 | ], 117 | "title": "Panel Title", 118 | "type": "barchart" 119 | }, 120 | { 121 | "datasource": { 122 | "type": "grafana", 123 | "uid": "grafana" 124 | }, 125 | "fieldConfig": { 126 | "defaults": { 127 | "color": { 128 | "mode": "palette-classic" 129 | }, 130 | "custom": { 131 | "axisCenteredZero": false, 132 | "axisColorMode": "text", 133 | "axisLabel": "", 134 | "axisPlacement": "auto", 135 | "barAlignment": 0, 136 | "drawStyle": "line", 137 | "fillOpacity": 0, 138 | "gradientMode": "none", 139 | "hideFrom": { 140 | "legend": false, 141 | "tooltip": false, 142 | "viz": false 143 | }, 144 | "lineInterpolation": "linear", 145 | "lineWidth": 1, 146 | "pointSize": 5, 147 | "scaleDistribution": { 148 | "type": "linear" 149 | }, 150 | "showPoints": "auto", 151 | "spanNulls": false, 152 | "stacking": { 153 | "group": "A", 154 | "mode": "none" 155 | }, 156 | "thresholdsStyle": { 157 | "mode": "off" 158 | } 159 | }, 160 | "mappings": [], 161 | "thresholds": { 162 | "mode": "absolute", 163 | "steps": [ 164 | { 165 | "color": "green", 166 | "value": null 167 | }, 168 | { 169 | "color": "red", 170 | "value": 80 171 | } 172 | ] 173 | } 174 | }, 175 | "overrides": [] 176 | }, 177 | "gridPos": { 178 | "h": 8, 179 | "w": 12, 180 | "x": 12, 181 | "y": 0 182 | }, 183 | "id": 2, 184 | "options": { 185 | "legend": { 186 | "calcs": [], 187 | "displayMode": "list", 188 | "placement": "bottom", 189 | "showLegend": true 190 | }, 191 | "tooltip": { 192 | "mode": "single", 193 | "sort": "none" 194 | } 195 | }, 196 | "targets": [ 197 | { 198 | "datasource": { 199 | "type": "datasource", 200 | "uid": "grafana" 201 | }, 202 | "queryType": "randomWalk", 203 | "refId": "A" 204 | } 205 | ], 206 | "title": "Panel Title", 207 | "type": "timeseries" 208 | } 209 | ], 210 | "refresh": "", 211 | "schemaVersion": 38, 212 | "style": "dark", 213 | "tags": [], 214 | "templating": { 215 | "list": [] 216 | }, 217 | "time": { 218 | "from": "now-6h", 219 | "to": "now" 220 | }, 221 | "timepicker": {}, 222 | "timezone": "", 223 | "title": "Test dashboard", 224 | "uid": "e9abc1a5-1b8f-4327-83e4-0b3c2b3722a9", 225 | "version": 6, 226 | "weekStart": "" 227 | } -------------------------------------------------------------------------------- /dashboards/test-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 4278, 22 | "links": [], 23 | "liveNow": false, 24 | "panels": [ 25 | { 26 | "datasource": { 27 | "type": "datasource", 28 | "uid": "grafana" 29 | }, 30 | "fieldConfig": { 31 | "defaults": { 32 | "color": { 33 | "mode": "palette-classic" 34 | }, 35 | "custom": { 36 | "axisCenteredZero": false, 37 | "axisColorMode": "text", 38 | "axisLabel": "", 39 | "axisPlacement": "auto", 40 | "fillOpacity": 80, 41 | "gradientMode": "none", 42 | "hideFrom": { 43 | "legend": false, 44 | "tooltip": false, 45 | "viz": false 46 | }, 47 | "lineWidth": 1, 48 | "scaleDistribution": { 49 | "type": "linear" 50 | }, 51 | "thresholdsStyle": { 52 | "mode": "off" 53 | } 54 | }, 55 | "mappings": [], 56 | "thresholds": { 57 | "mode": "absolute", 58 | "steps": [ 59 | { 60 | "color": "green", 61 | "value": null 62 | }, 63 | { 64 | "color": "red", 65 | "value": 80 66 | } 67 | ] 68 | } 69 | }, 70 | "overrides": [] 71 | }, 72 | "gridPos": { 73 | "h": 8, 74 | "w": 12, 75 | "x": 0, 76 | "y": 0 77 | }, 78 | "id": 1, 79 | "options": { 80 | "barRadius": 0, 81 | "barWidth": 0.97, 82 | "fullHighlight": false, 83 | "groupWidth": 0.7, 84 | "legend": { 85 | "calcs": [], 86 | "displayMode": "list", 87 | "placement": "bottom", 88 | "showLegend": true 89 | }, 90 | "orientation": "auto", 91 | "showValue": "auto", 92 | "stacking": "none", 93 | "tooltip": { 94 | "mode": "single", 95 | "sort": "none" 96 | }, 97 | "xTickLabelRotation": 0, 98 | "xTickLabelSpacing": 0 99 | }, 100 | "targets": [ 101 | { 102 | "channel": "plugin/testdata/random-2s-stream", 103 | "datasource": { 104 | "type": "datasource", 105 | "uid": "grafana" 106 | }, 107 | "filter": { 108 | "fields": [ 109 | "Time", 110 | "Min" 111 | ] 112 | }, 113 | "queryType": "randomWalk", 114 | "refId": "A" 115 | } 116 | ], 117 | "title": "Panel Title", 118 | "type": "barchart" 119 | }, 120 | { 121 | "datasource": { 122 | "type": "grafana", 123 | "uid": "grafana" 124 | }, 125 | "fieldConfig": { 126 | "defaults": { 127 | "color": { 128 | "mode": "palette-classic" 129 | }, 130 | "custom": { 131 | "axisCenteredZero": false, 132 | "axisColorMode": "text", 133 | "axisLabel": "", 134 | "axisPlacement": "auto", 135 | "barAlignment": 0, 136 | "drawStyle": "line", 137 | "fillOpacity": 0, 138 | "gradientMode": "none", 139 | "hideFrom": { 140 | "legend": false, 141 | "tooltip": false, 142 | "viz": false 143 | }, 144 | "lineInterpolation": "linear", 145 | "lineWidth": 1, 146 | "pointSize": 5, 147 | "scaleDistribution": { 148 | "type": "linear" 149 | }, 150 | "showPoints": "auto", 151 | "spanNulls": false, 152 | "stacking": { 153 | "group": "A", 154 | "mode": "none" 155 | }, 156 | "thresholdsStyle": { 157 | "mode": "off" 158 | } 159 | }, 160 | "mappings": [], 161 | "thresholds": { 162 | "mode": "absolute", 163 | "steps": [ 164 | { 165 | "color": "green", 166 | "value": null 167 | }, 168 | { 169 | "color": "red", 170 | "value": 80 171 | } 172 | ] 173 | } 174 | }, 175 | "overrides": [] 176 | }, 177 | "gridPos": { 178 | "h": 8, 179 | "w": 12, 180 | "x": 12, 181 | "y": 0 182 | }, 183 | "id": 2, 184 | "options": { 185 | "legend": { 186 | "calcs": [], 187 | "displayMode": "list", 188 | "placement": "bottom", 189 | "showLegend": true 190 | }, 191 | "tooltip": { 192 | "mode": "single", 193 | "sort": "none" 194 | } 195 | }, 196 | "targets": [ 197 | { 198 | "datasource": { 199 | "type": "datasource", 200 | "uid": "grafana" 201 | }, 202 | "queryType": "randomWalk", 203 | "refId": "A" 204 | } 205 | ], 206 | "title": "Panel Title", 207 | "type": "timeseries" 208 | } 209 | ], 210 | "refresh": "", 211 | "schemaVersion": 38, 212 | "style": "dark", 213 | "tags": [], 214 | "templating": { 215 | "list": [] 216 | }, 217 | "time": { 218 | "from": "now-6h", 219 | "to": "now" 220 | }, 221 | "timepicker": {}, 222 | "timezone": "", 223 | "title": "Test dashboard", 224 | "uid": "e9abc1a5-1b8f-4327-83e4-0b3c2b3722a9", 225 | "version": 6, 226 | "weekStart": "" 227 | } -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/main/.schema/devbox.schema.json", 3 | "packages": [ 4 | "nodejs_18@18.20", 5 | "yarn@1.22", 6 | "vsce@2.26" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /devbox.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfile_version": "1", 3 | "packages": { 4 | "nodejs_18@18.20": { 5 | "last_modified": "2024-09-10T15:01:03Z", 6 | "plugin_version": "0.0.2", 7 | "resolved": "github:NixOS/nixpkgs/5ed627539ac84809c78b2dd6d26a5cebeb5ae269#nodejs_18", 8 | "source": "devbox-search", 9 | "version": "18.20.4", 10 | "systems": { 11 | "aarch64-darwin": { 12 | "outputs": [ 13 | { 14 | "name": "out", 15 | "path": "/nix/store/mq4a000w2661rsz18bc66kspz46xg36i-nodejs-18.20.4", 16 | "default": true 17 | }, 18 | { 19 | "name": "libv8", 20 | "path": "/nix/store/m6bjaxvy75snd24fk3kki98f80k0zazj-nodejs-18.20.4-libv8" 21 | } 22 | ], 23 | "store_path": "/nix/store/mq4a000w2661rsz18bc66kspz46xg36i-nodejs-18.20.4" 24 | }, 25 | "aarch64-linux": { 26 | "outputs": [ 27 | { 28 | "name": "out", 29 | "path": "/nix/store/f92p8im867b0ih7x75fhvzq1n03a88nm-nodejs-18.20.4", 30 | "default": true 31 | }, 32 | { 33 | "name": "libv8", 34 | "path": "/nix/store/bd2r8dn44lx8bkjikf7y0nlg9swyn90d-nodejs-18.20.4-libv8" 35 | } 36 | ], 37 | "store_path": "/nix/store/f92p8im867b0ih7x75fhvzq1n03a88nm-nodejs-18.20.4" 38 | }, 39 | "x86_64-darwin": { 40 | "outputs": [ 41 | { 42 | "name": "out", 43 | "path": "/nix/store/6ig5ijfhc8amfwh3y9kdh7yrfcv6wnzh-nodejs-18.20.4", 44 | "default": true 45 | }, 46 | { 47 | "name": "libv8", 48 | "path": "/nix/store/74gg33cz9cksv940ffxrbx75z160k5cr-nodejs-18.20.4-libv8" 49 | } 50 | ], 51 | "store_path": "/nix/store/6ig5ijfhc8amfwh3y9kdh7yrfcv6wnzh-nodejs-18.20.4" 52 | }, 53 | "x86_64-linux": { 54 | "outputs": [ 55 | { 56 | "name": "out", 57 | "path": "/nix/store/lgw9z5sf9n6pm2dqmd92z4gq4irslnq1-nodejs-18.20.4", 58 | "default": true 59 | }, 60 | { 61 | "name": "libv8", 62 | "path": "/nix/store/rgrg7ccyvbzj6v9fdhbzvr3di2hl4458-nodejs-18.20.4-libv8" 63 | } 64 | ], 65 | "store_path": "/nix/store/lgw9z5sf9n6pm2dqmd92z4gq4irslnq1-nodejs-18.20.4" 66 | } 67 | } 68 | }, 69 | "vsce@2.26": { 70 | "last_modified": "2024-05-22T06:18:38Z", 71 | "resolved": "github:NixOS/nixpkgs/3f316d2a50699a78afe5e77ca486ad553169061e#vsce", 72 | "source": "devbox-search", 73 | "version": "2.26.1", 74 | "systems": { 75 | "aarch64-darwin": { 76 | "outputs": [ 77 | { 78 | "name": "out", 79 | "path": "/nix/store/ijx8dlx403q219b0981vickkgk2vvxfg-vsce-2.26.1", 80 | "default": true 81 | } 82 | ], 83 | "store_path": "/nix/store/ijx8dlx403q219b0981vickkgk2vvxfg-vsce-2.26.1" 84 | }, 85 | "aarch64-linux": { 86 | "outputs": [ 87 | { 88 | "name": "out", 89 | "path": "/nix/store/0yicgcnvrqyfdh208lmylnad6g03syda-vsce-2.26.1", 90 | "default": true 91 | } 92 | ], 93 | "store_path": "/nix/store/0yicgcnvrqyfdh208lmylnad6g03syda-vsce-2.26.1" 94 | }, 95 | "x86_64-darwin": { 96 | "outputs": [ 97 | { 98 | "name": "out", 99 | "path": "/nix/store/48w3k1sz6r85n77g2pcn8f4dn6j18mb5-vsce-2.26.1", 100 | "default": true 101 | } 102 | ], 103 | "store_path": "/nix/store/48w3k1sz6r85n77g2pcn8f4dn6j18mb5-vsce-2.26.1" 104 | }, 105 | "x86_64-linux": { 106 | "outputs": [ 107 | { 108 | "name": "out", 109 | "path": "/nix/store/aw41wckwmyd0vjb4fmlmxng46cdd1gwz-vsce-2.26.1", 110 | "default": true 111 | } 112 | ], 113 | "store_path": "/nix/store/aw41wckwmyd0vjb4fmlmxng46cdd1gwz-vsce-2.26.1" 114 | } 115 | } 116 | }, 117 | "yarn@1.22": { 118 | "last_modified": "2024-09-10T15:01:03Z", 119 | "resolved": "github:NixOS/nixpkgs/5ed627539ac84809c78b2dd6d26a5cebeb5ae269#yarn", 120 | "source": "devbox-search", 121 | "version": "1.22.22", 122 | "systems": { 123 | "aarch64-darwin": { 124 | "outputs": [ 125 | { 126 | "name": "out", 127 | "path": "/nix/store/6j0fflbrszc5k5kd7m36mn9ga6ds8zmr-yarn-1.22.22", 128 | "default": true 129 | } 130 | ], 131 | "store_path": "/nix/store/6j0fflbrszc5k5kd7m36mn9ga6ds8zmr-yarn-1.22.22" 132 | }, 133 | "aarch64-linux": { 134 | "outputs": [ 135 | { 136 | "name": "out", 137 | "path": "/nix/store/d1f5qfjkc24v2ng46x0qjsbiwrdfa6d3-yarn-1.22.22", 138 | "default": true 139 | } 140 | ], 141 | "store_path": "/nix/store/d1f5qfjkc24v2ng46x0qjsbiwrdfa6d3-yarn-1.22.22" 142 | }, 143 | "x86_64-darwin": { 144 | "outputs": [ 145 | { 146 | "name": "out", 147 | "path": "/nix/store/vfi9zsfiwq5yia28nx04zdmr9ij6nhi3-yarn-1.22.22", 148 | "default": true 149 | } 150 | ], 151 | "store_path": "/nix/store/vfi9zsfiwq5yia28nx04zdmr9ij6nhi3-yarn-1.22.22" 152 | }, 153 | "x86_64-linux": { 154 | "outputs": [ 155 | { 156 | "name": "out", 157 | "path": "/nix/store/9yd6frl42syv6vncdy619zblj2vg406g-yarn-1.22.22", 158 | "default": true 159 | } 160 | ], 161 | "store_path": "/nix/store/9yd6frl42syv6vncdy619zblj2vg406g-yarn-1.22.22" 162 | } 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /how-it-works.md: -------------------------------------------------------------------------------- 1 | # Extension communication with Grafana 2 | 3 | The below diagram explains how this extension communicates with Grafana. 4 | 5 | For best viewing, view this page on GitHub. 6 | 7 | ```mermaid 8 | sequenceDiagram 9 | participant Webview as Webview
(inside the VS Code Extension) 10 | participant Iframe as Iframe (Grafana)
(rendered inside the extension's webview) 11 | participant ProxyServer as Proxy server
(running inside the extension) 12 | participant Grafana as Grafana Instance
(running outside the extension) 13 | participant FileSystem as File system 14 | 15 | Note over ProxyServer: Starts on random port 16 | Webview->>Iframe: Render an iframe for Grafana. Callback URL to the proxy is an iframe src URL param 17 | Iframe->>ProxyServer: Requests HTML dashboard page/etc 18 | ProxyServer->>Grafana: Requests HTML dashboards page/etc 19 | Iframe->>ProxyServer: Request to retrieve the JSON for opened dashboard 20 | FileSystem->>ProxyServer: Retrieve JSON 21 | ProxyServer-->>Iframe: JSON for opened dashboard 22 | Iframe->>ProxyServer: Edited dashboard JSON on save 23 | ProxyServer->>FileSystem: Edited dashboard JSON 24 | ``` 25 | -------------------------------------------------------------------------------- /media/grafana_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-vs-code-extension/b62d3c8b604b1506f7e39ed31329dacae93b7d54/media/grafana_logo.png -------------------------------------------------------------------------------- /media/preview_commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-vs-code-extension/b62d3c8b604b1506f7e39ed31329dacae93b7d54/media/preview_commands.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grafana-vscode", 3 | "author": "Grafana Labs", 4 | "displayName": "Grafana", 5 | "description": "Grafana Editor", 6 | "icon": "public/grafana_icon.png", 7 | "version": "0.0.20", 8 | "license": "Apache-2.0", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/grafana/grafana-vs-code-extension" 12 | }, 13 | "engines": { 14 | "vscode": "^1.76.0" 15 | }, 16 | "publisher": "Grafana", 17 | "categories": [ 18 | "Visualization", 19 | "Snippets" 20 | ], 21 | "keywords": [ 22 | "Grafana", 23 | "dashboards" 24 | ], 25 | "activationEvents": [ 26 | "onStartupFinished" 27 | ], 28 | "main": "./dist/extension.js", 29 | "contributes": { 30 | "customEditors": [ 31 | { 32 | "viewType": "grafana.dashboard", 33 | "displayName": "Grafana", 34 | "selector": [ 35 | { 36 | "filenamePattern": "*.grafana" 37 | } 38 | ] 39 | } 40 | ], 41 | "commands": [ 42 | { 43 | "command": "grafana-vscode.openUrl", 44 | "title": "Grafana: Edit in Grafana" 45 | } 46 | ], 47 | "menus": { 48 | "commandPalette": [ 49 | { 50 | "command": "grafana-vscode.openUrl", 51 | "when": "resourceExtname == .json || resourceExtname == .yaml || resourceExtname == .yml" 52 | } 53 | ], 54 | "explorer/context": [ 55 | { 56 | "command": "grafana-vscode.openUrl", 57 | "when": "resourceExtname == .json || resourceExtname == .yaml || resourceExtname == .yml" 58 | } 59 | ] 60 | }, 61 | "snippets": [ 62 | { 63 | "language": "json", 64 | "path": "./snippets.json" 65 | } 66 | ], 67 | "iconThemes": [ 68 | { 69 | "id": "grafana", 70 | "label": "Grafana", 71 | "path": "./public/icon-theme.json" 72 | } 73 | ], 74 | "configuration": { 75 | "title": "Grafana", 76 | "properties": { 77 | "grafana-vscode.URL": { 78 | "type": "string", 79 | "default": "http://localhost:3000", 80 | "description": "Grafana instance URL", 81 | "order": 1 82 | }, 83 | "grafana-vscode.service-account-token": { 84 | "type": "boolean", 85 | "default": true, 86 | "markdownDescription": "A service account token for your Grafana instance. Click the link below to add this to secure storage.\n\n[Set your token, securely](command:grafana-vscode.setPassword)", 87 | "order": 2 88 | }, 89 | "grafana-vscode.theme": { 90 | "type": "string", 91 | "default": "inherit", 92 | "enum": [ 93 | "inherit", 94 | "fixed", 95 | "dark", 96 | "light" 97 | ], 98 | "enumDescriptions": [ 99 | "Inherit Grafana theme from VSCode", 100 | "Use Grafana's own default theme", 101 | "Use dark Grafana theme", 102 | "Use light Grafana theme" 103 | ] 104 | }, 105 | "grafana-vscode.telemetry": { 106 | "type": "boolean", 107 | "default": true, 108 | "markdownDescription": "Enable basic telemetry. All data is anonymous and only used to help with feature prioritization/gloating/etc.", 109 | "order": 3 110 | } 111 | } 112 | }, 113 | "walkthroughs": [ 114 | { 115 | "id": "intro", 116 | "title": "Getting started with VS Code for Grafana", 117 | "description": "Just a few more steps before some Grafana magic in your editor!", 118 | "steps": [ 119 | { 120 | "id": "grafana_url_setting", 121 | "title": "Provide the URL of your Grafana instance", 122 | "description": "This extension uses data held by your Grafana instance to enable local previews of dashboards. **This access is purely read-only**, nothing will ever be written to the istance.\nIf you are using a local Grafana instance, the default value is ``http://localhost:3000``.\n[Configure Grafana instance URL](command:grafana-vscode.setGrafanaURL)", 123 | "media": { 124 | "image": "media/grafana_logo.png", 125 | "altText": "Grafana logo" 126 | }, 127 | "completionEvents": ["onSettingChanged:grafana-vscode.URL"] 128 | }, 129 | { 130 | "id": "grafana_service_account_token_setting", 131 | "title": "Provide a service account token used to authenticate to Grafana", 132 | "description": "The steps required to create a service account and an associated token can be found in [Grafana's documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/#create-a-service-account-in-grafana).\n[Configure Grafana service account token](command:grafana-vscode.setPassword)", 133 | "media": { 134 | "image": "media/grafana_logo.png", 135 | "altText": "Grafana logo" 136 | }, 137 | "completionEvents": ["onSettingChanged:grafana-vscode.service-account-token"] 138 | }, 139 | { 140 | "id": "preview_dashboards", 141 | "title": "Preview dashboards", 142 | "description": "Dashboards can be previewed locally, by right-clicking on a file or by calling the \"Edit in Grafana\" command.", 143 | "media": { 144 | "image": "media/preview_commands.png", 145 | "altText": "Preview commands" 146 | } 147 | } 148 | ] 149 | } 150 | ] 151 | }, 152 | "scripts": { 153 | "vscode:prepublish": "yarn run package", 154 | "compile": "webpack", 155 | "watch": "webpack --watch", 156 | "package": "webpack --mode production --devtool hidden-source-map", 157 | "compile-tests": "tsc -p . --outDir out", 158 | "watch-tests": "tsc -p . -w --outDir out", 159 | "pretest": "yarn run compile-tests && yarn run compile && yarn run lint", 160 | "lint": "eslint src --ext ts", 161 | "test": "node ./out/test/runTest.js" 162 | }, 163 | "devDependencies": { 164 | "@types/cors": "^2.8.13", 165 | "@types/express": "^4.17.17", 166 | "@types/glob": "^8.1.0", 167 | "@types/http-proxy": "^1.17.10", 168 | "@types/mocha": "^10.0.1", 169 | "@types/node": "18.x", 170 | "@types/source-map-support": "^0.5.8", 171 | "@types/uuid": "^9.0.6", 172 | "@types/vscode": "^1.76.0", 173 | "@typescript-eslint/eslint-plugin": "^5.53.0", 174 | "@typescript-eslint/parser": "^5.53.0", 175 | "@vscode/test-electron": "^2.2.3", 176 | "eslint": "^8.34.0", 177 | "glob": "^8.1.0", 178 | "mocha": "^10.2.0", 179 | "prettier": "3.0.2", 180 | "ts-loader": "^9.4.2", 181 | "typescript": "^4.9.5", 182 | "webpack": "^5.75.0", 183 | "webpack-cli": "^5.0.1" 184 | }, 185 | "dependencies": { 186 | "cors": "^2.8.5", 187 | "express": "^4.21.2", 188 | "http-proxy": "^1.18.1", 189 | "http-proxy-middleware": "^2.0.6", 190 | "open": "^8.4.2", 191 | "source-map-support": "^0.5.21", 192 | "transformer-proxy": "^0.3.5", 193 | "uuid": "^9.0.1", 194 | "yaml": "^2.7.0" 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /public/error.html: -------------------------------------------------------------------------------- 1 | 23 |

A problem occurred while connecting to Grafana

${error} 24 |

25 | 26 | -------------------------------------------------------------------------------- /public/extensions-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-vs-code-extension/b62d3c8b604b1506f7e39ed31329dacae93b7d54/public/extensions-icon.png -------------------------------------------------------------------------------- /public/grafana_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-vs-code-extension/b62d3c8b604b1506f7e39ed31329dacae93b7d54/public/grafana_icon.png -------------------------------------------------------------------------------- /public/icon-theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "iconDefinitions": { 3 | "grafana": { 4 | "iconPath": "./grafana_icon.png" 5 | } 6 | }, 7 | "fileExtensions": { 8 | "grafana": "grafana" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /public/iframe.css: -------------------------------------------------------------------------------- 1 | 2 | .sidemenu { 3 | display: none !important; 4 | } 5 | 6 | [aria-label="Share dashboard or panel"] { 7 | display: none !important; 8 | } 9 | 10 | [aria-label="Search links"] { 11 | pointer-events: none; 12 | cursor: default; 13 | } 14 | 15 | [aria-label="Breadcrumbs"] { 16 | pointer-events: none; 17 | cursor: default; 18 | } 19 | 20 | [aria-label="Toggle menu"] { 21 | pointer-events: none; 22 | cursor: default; 23 | } 24 | 25 | [title="Toggle top search bar"] { 26 | display: none !important; 27 | } 28 | 29 | .main-view > div:first-of-type > div:first-of-type { 30 | display: none !important; 31 | } -------------------------------------------------------------------------------- /public/webview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "New Grafana dashboard": { 3 | "prefix": [ 4 | "grafana_dashboard", 5 | "dashboard" 6 | ], 7 | "description": "Create a new Grafana dashboard", 8 | "isFileTemplate": true, 9 | "body": [ 10 | "{", 11 | " \"title\": \"${1:New dashboard title}\",", 12 | " \"uid\": \"${2:new-dashboard-uid}\",", 13 | " \"annotations\": {", 14 | " \"list\": [", 15 | " {", 16 | " \"builtIn\": 1,", 17 | " \"datasource\": {", 18 | " \"type\": \"grafana\",", 19 | " \"uid\": \"-- Grafana --\"", 20 | " },", 21 | " \"enable\": true,", 22 | " \"hide\": true,", 23 | " \"iconColor\": \"rgba(0, 211, 255, 1)\",", 24 | " \"name\": \"Annotations & Alerts\",", 25 | " \"type\": \"dashboard\"", 26 | " }", 27 | " ]", 28 | " },", 29 | " \"editable\": true,", 30 | " \"fiscalYearStartMonth\": 0,", 31 | " \"graphTooltip\": 0,", 32 | " \"links\": [],", 33 | " \"panels\": [],", 34 | " \"preload\": false,", 35 | " \"schemaVersion\": 39,", 36 | " \"tags\": [],", 37 | " \"templating\": {", 38 | " \"list\": []", 39 | " },", 40 | " \"time\": {", 41 | " \"from\": \"now-6h\",", 42 | " \"to\": \"now\"", 43 | " },", 44 | " \"timepicker\": {},", 45 | " \"timezone\": \"browser\",", 46 | " \"version\": 0,", 47 | " \"weekStart\": \"\"", 48 | "}" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/editor.ts: -------------------------------------------------------------------------------- 1 | import vscode, { ColorThemeKind } from "vscode"; 2 | import fs from "fs"; 3 | import { port } from "./server"; 4 | import { Resource } from "./grafana"; 5 | 6 | export class GrafanaEditorProvider implements vscode.CustomTextEditorProvider { 7 | public static readonly viewType = "grafana.dashboard"; 8 | 9 | private static webviewContent = ""; 10 | 11 | public static register(context: vscode.ExtensionContext): vscode.Disposable { 12 | const provider = new GrafanaEditorProvider(context); 13 | const providerRegistration = vscode.window.registerCustomEditorProvider( 14 | GrafanaEditorProvider.viewType, 15 | provider, 16 | { 17 | webviewOptions: { 18 | retainContextWhenHidden: true, 19 | }, 20 | }, 21 | ); 22 | this.webviewContent = fs.readFileSync( 23 | context.asAbsolutePath("public/webview.html"), 24 | "utf-8", 25 | ); 26 | this.webviewContent = this.webviewContent.replaceAll("${editor}", "VSCode"); 27 | 28 | return providerRegistration; 29 | } 30 | 31 | constructor(private readonly context: vscode.ExtensionContext) { } 32 | 33 | /** 34 | * Called when our custom editor is opened. 35 | */ 36 | public async resolveCustomTextEditor( 37 | document: vscode.TextDocument, 38 | webviewPanel: vscode.WebviewPanel, 39 | _token: vscode.CancellationToken, 40 | ) { 41 | webviewPanel.webview.options = { 42 | enableScripts: true, 43 | }; 44 | 45 | webviewPanel.webview.html = this.getHtmlForWebview(document); 46 | } 47 | 48 | private getTheme(): string { 49 | const settings = vscode.workspace.getConfiguration("grafana-vscode"); 50 | const theme = settings.get("theme"); 51 | if (theme === "dark" || theme === "light") { 52 | return `theme=${theme}&`; 53 | } 54 | if (theme === "fixed") { 55 | return ""; 56 | } 57 | 58 | const kind = vscode.window.activeColorTheme.kind; 59 | if (kind === ColorThemeKind.Light || kind === ColorThemeKind.HighContrastLight) { 60 | return "theme=light&"; 61 | } 62 | 63 | return "theme=dark&"; 64 | } 65 | 66 | /** 67 | * Get the static html used for the editor webviews. 68 | */ 69 | private getHtmlForWebview(document: vscode.TextDocument): string { 70 | try { 71 | const resource = Resource.fromDocument(document); 72 | 73 | return GrafanaEditorProvider.webviewContent 74 | .replaceAll("${port}", port.toString()) 75 | .replaceAll("${theme}", this.getTheme()) 76 | .replaceAll("${filename}", resource.filename) 77 | .replaceAll("${uid}", resource.uid()); 78 | } catch (err) { 79 | return this.errorView(String(err)); 80 | } 81 | } 82 | 83 | private errorView(errorMessage: string): string { 84 | return fs.readFileSync( 85 | this.context.asAbsolutePath("public/error.html"), 86 | "utf-8", 87 | ).replaceAll('${error}', errorMessage); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import vscode from "vscode"; 4 | import { startServer, restartServer, stopServer, TOKEN_SECRET } from "./server"; 5 | import { GrafanaEditorProvider } from "./editor"; 6 | import { install as installSourceMapSupport } from 'source-map-support'; 7 | import { sendTelemetry } from "./telemetry"; 8 | import { setVersion } from "./util"; 9 | 10 | // This method is called when your extension is activated 11 | // Your extension is activated the very first time the command is executed 12 | export async function activate(ctx: vscode.ExtensionContext) { 13 | setVersion(ctx.extension.packageJSON.version); 14 | startServer(ctx.secrets, ctx.extensionPath); 15 | 16 | ctx.subscriptions.push(GrafanaEditorProvider.register(ctx)); 17 | 18 | ctx.subscriptions.push( 19 | vscode.commands.registerCommand( 20 | "grafana-vscode.openUrl", 21 | (uri?: vscode.Uri) => { 22 | sendTelemetry(ctx); 23 | 24 | // This command can be invoked from a contextual menu, in which case uri 25 | // has a value. 26 | // It can also be invoked from the command palette, in which case we try to find 27 | // the active document. 28 | let actualUri = uri || vscode.window.activeTextEditor?.document.uri; 29 | if (!actualUri) { 30 | return; 31 | } 32 | 33 | vscode.commands.executeCommand( 34 | "vscode.openWith", 35 | actualUri, 36 | GrafanaEditorProvider.viewType, 37 | ); 38 | }), 39 | ); 40 | 41 | vscode.workspace.onDidChangeConfiguration(event => { 42 | if (event.affectsConfiguration("grafana-vscode")) { 43 | restartServer(ctx.secrets, ctx.extensionPath); 44 | } 45 | }); 46 | 47 | vscode.commands.registerCommand('grafana-vscode.setGrafanaURL', async () => { 48 | const instanceURL = await vscode.window.showInputBox({ 49 | title: "Grafana instance URL", 50 | placeHolder: "http://localhost:3000", 51 | }) ?? ''; 52 | await vscode.workspace.getConfiguration('grafana-vscode').update('URL', instanceURL); 53 | }); 54 | 55 | vscode.commands.registerCommand('grafana-vscode.setPassword', async () => { 56 | const passwordInput = await vscode.window.showInputBox({ 57 | password: true, 58 | placeHolder: "My Grafana service account token", 59 | title: "Enter the service account token for your Grafana instance. This value will be stored securely in your operating system's secure key store." 60 | }) ?? ''; 61 | await ctx.secrets.store(TOKEN_SECRET, passwordInput); 62 | }); 63 | 64 | installSourceMapSupport(); 65 | } 66 | 67 | // This method is called when your extension is deactivated 68 | export function deactivate() { 69 | stopServer(); 70 | } 71 | -------------------------------------------------------------------------------- /src/grafana.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import vscode from 'vscode'; 3 | import YAML from 'yaml'; 4 | 5 | type Format = 'json' | 'yaml'; 6 | type Envelope = 'none' | 'grizzly' | 'http_api'; 7 | 8 | export class Resource { 9 | private readonly viewType = "grafana.dashboard"; 10 | 11 | public static fromDocument(document: vscode.TextDocument): Resource { 12 | return Resource.decode(document.fileName, document.getText()); 13 | } 14 | 15 | public static fromFile(filename: string): Resource { 16 | return Resource.decode(filename, fs.readFileSync(filename, 'utf-8').toString()); 17 | } 18 | 19 | public static decode(filename: string, content: string): Resource { 20 | const extension = filename.slice(filename.lastIndexOf(".")); 21 | let resourceData: any = {}; 22 | let format: Format = 'json'; 23 | 24 | switch (extension) { 25 | case '.json': 26 | resourceData = JSON.parse(content); 27 | break; 28 | case '.yaml': 29 | case '.yml': 30 | resourceData = YAML.parse(content); 31 | format = 'yaml'; 32 | break; 33 | default: 34 | throw new Error(`unsupported extension '${extension}`); 35 | } 36 | 37 | return Resource.unwrap(filename, format, resourceData); 38 | } 39 | 40 | private static unwrap(filename: string, format: Format, resourceData: any): Resource { 41 | // no apparent envelope 42 | if (resourceData.uid) { 43 | return new Resource(filename, format, 'none', resourceData); 44 | } 45 | 46 | // HTTP API envelope 47 | // See https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#get-dashboard-by-uid 48 | if (resourceData.dashboard && resourceData.meta) { 49 | if (!resourceData.dashboard.uid) { 50 | throw new Error(`malformed HTTP envelope in '${filename}: dashboard.uid field not found`); 51 | } 52 | 53 | return new Resource(filename, format, 'http_api', resourceData); 54 | } 55 | 56 | // grizzly envelope 57 | if (resourceData.apiVersion === 'grizzly.grafana.com/v1alpha1') { 58 | if (!resourceData.metadata || !resourceData.metadata.name) { 59 | throw new Error(`malformed grizzly envelope in '${filename}: metadata.name field not found`); 60 | } 61 | if (!resourceData.spec) { 62 | throw new Error(`malformed grizzly envelope in '${filename}: spec field not found`); 63 | } 64 | 65 | return new Resource(filename, format, 'grizzly', resourceData); 66 | } 67 | 68 | throw new Error(`could not parse resource in '${filename}: unrecognized format`); 69 | } 70 | 71 | constructor( 72 | public readonly filename: string, 73 | private readonly format: Format, 74 | private readonly envelope: Envelope, 75 | private readonly data: any, 76 | ) { } 77 | 78 | public uid(): string { 79 | if (this.envelope === 'none') { 80 | return this.data.uid; 81 | } 82 | 83 | if (this.envelope === 'http_api') { 84 | return this.data.dashboard.uid; 85 | } 86 | 87 | // grizzly style 88 | return this.data.metadata.name; 89 | } 90 | 91 | public spec(): any { 92 | if (this.envelope === 'none') { 93 | return this.data; 94 | } 95 | if (this.envelope === 'http_api') { 96 | return this.data.dashboard; 97 | } 98 | 99 | // grizzly style 100 | return this.data.spec; 101 | } 102 | 103 | public withSpec(newSpec: any): Resource { 104 | if (this.envelope === 'grizzly') { 105 | const newData = {...this.data, ...{spec: newSpec}}; 106 | return new Resource(this.filename, this.format, this.envelope, newData); 107 | } 108 | 109 | if (this.envelope === 'http_api') { 110 | const newData = {...this.data, ...{dashboard: newSpec}}; 111 | return new Resource(this.filename, this.format, this.envelope, newData); 112 | } 113 | 114 | return new Resource(this.filename, this.format, this.envelope, newSpec); 115 | } 116 | 117 | public write(): Promise { 118 | let content = ''; 119 | switch (this.format) { 120 | case "json": 121 | content = JSON.stringify(this.data, null, 2); 122 | break; 123 | case "yaml": 124 | content = YAML.stringify(this.data, null, {indent: 4}); 125 | break; 126 | } 127 | 128 | return fs.promises.writeFile(this.filename, content); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request, NextFunction } from "express"; 2 | 3 | export function detectRequestSource( 4 | req: Request, 5 | res: Response, 6 | next: NextFunction, 7 | ) { 8 | const userAgent = req.headers["user-agent"]; 9 | 10 | if ((userAgent?.includes("Code") || userAgent?.includes("code")) 11 | && userAgent?.includes("Electron")) { 12 | next(); 13 | } else { 14 | res.status(403).send("Access Denied"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import { Server, createServer } from "http"; 3 | import { createProxyServer } from "http-proxy"; 4 | import cors from "cors"; 5 | import fs from "fs"; 6 | import path from "path"; 7 | import vscode from "vscode"; 8 | import { detectRequestSource } from "./middleware"; 9 | import * as util from "./util"; 10 | import { Resource } from "./grafana"; 11 | 12 | export let port = 0; 13 | 14 | let server: Server; 15 | 16 | export const TOKEN_SECRET = "grafana-vscode.token"; 17 | 18 | export async function startServer(secrets: vscode.SecretStorage, extensionPath: string) { 19 | const settings = vscode.workspace.getConfiguration("grafana-vscode"); 20 | const token = await secrets.get(TOKEN_SECRET); 21 | let URL = String(settings.get("URL")); 22 | if (URL.slice(-1) === "/") { 23 | URL = URL.slice(0, -1); 24 | } 25 | 26 | const corsOptions = { 27 | origin: `http://localhost:${port}`, 28 | optionsSuccessStatus: 200, 29 | }; 30 | 31 | const app = express(); 32 | app.use(detectRequestSource); 33 | server = createServer(app); 34 | 35 | const proxy = createProxyServer({ 36 | target: URL, 37 | changeOrigin: !URL.includes("localhost"), 38 | ws: true, 39 | headers: { 40 | // eslint-disable-next-line @typescript-eslint/naming-convention 41 | Authorization: `Bearer ${token}`, 42 | // eslint-disable-next-line @typescript-eslint/naming-convention 43 | 'User-Agent': util.getUserAgent(), 44 | }, 45 | }); 46 | 47 | server.on("upgrade", function (req, socket, head) { 48 | proxy.ws(req, socket, head, {}); 49 | }); 50 | 51 | const sendErrorPage = (res: express.Response, message: string) => { 52 | const errorFile = path.join(extensionPath, "public/error.html"); 53 | 54 | res.send( 55 | fs.readFileSync(errorFile, "utf-8") 56 | .replaceAll("${error}", message), 57 | ); 58 | }; 59 | 60 | /* 61 | * Note, this method avoids using `proxy.web`, implementing its own proxy 62 | * event using Axios. This is because Grafana returns `X-Frame-Options: deny` 63 | * which breaks our ability to place Grafana inside an iframe. `http-proxy` 64 | * will not remove that header once it is added. Therefore we need a different 65 | * form of proxy. 66 | * 67 | * This security protection does not apply to this situation - given we own 68 | * both the connection to the backend as well as the webview. Therefore 69 | * it is reasonable remove this header in this context. 70 | * 71 | * This method also doubles as connection verification. If an issue is 72 | * encountered connecting to Grafana, rather than reporting an HTTP error, 73 | * it returns an alternate HTML page to the user explaining the error, and 74 | * offering a "refresh" option. 75 | */ 76 | app.get("/d/:uid/:slug", async function (req: Request, res: Response) { 77 | let msg = ""; 78 | if (URL === "") { 79 | msg += "

Error: URL is not defined

"; 80 | } 81 | if (token === "") { 82 | msg += "

Warning: No service account token specified.

"; 83 | } 84 | 85 | try { 86 | const response = await fetch(URL + req.url, { 87 | headers: { 88 | // eslint-disable-next-line @typescript-eslint/naming-convention 89 | Authorization: `Bearer ${token}`, 90 | // eslint-disable-next-line @typescript-eslint/naming-convention 91 | 'User-Agent': util.getUserAgent(), 92 | }, 93 | }); 94 | res.send(await response.text()); 95 | 96 | if (!response.ok && response.status === 302) { 97 | sendErrorPage(res, msg + "

Authentication error

"); 98 | } else if (!response.ok) { 99 | sendErrorPage(res, msg + `

${response.status} ${response.statusText}

`); 100 | } 101 | } catch (e) { 102 | if (e instanceof Error) { 103 | sendErrorPage(res, msg + `

${e.message}

`); 104 | } else { 105 | sendErrorPage(res, msg + "

" + String(e) + "

"); 106 | } 107 | } 108 | }); 109 | 110 | app.get( 111 | "/api/dashboards/uid/:uid", 112 | express.json(), 113 | cors(corsOptions), 114 | (req: Request, res: Response) => { 115 | const refererParams = new URLSearchParams(req.headers.referer); 116 | const filename = refererParams.get("filename"); 117 | if (filename === null) { 118 | console.log("Filename not specified in referer"); 119 | res.sendStatus(500); 120 | return; 121 | } 122 | 123 | const resource = Resource.fromFile(filename); 124 | 125 | res.send({ 126 | dashboard: resource.spec(), 127 | meta: { 128 | isStarred: false, 129 | folderId: 0, 130 | folderUid: "", 131 | url: `/d/${resource.uid()}/slug`, 132 | }, 133 | }); 134 | }, 135 | ); 136 | 137 | app.post( 138 | "/api/dashboards/db/", 139 | express.json(), 140 | cors(corsOptions), 141 | (req: Request, res: Response) => { 142 | const refererParams = new URLSearchParams(req.headers.referer); 143 | const filename = refererParams.get("filename"); 144 | if (!filename) { 145 | console.error('expected filename in referer parameters'); 146 | res.send(500); 147 | return; 148 | } 149 | 150 | const resource = Resource.fromFile(filename).withSpec(req.body.dashboard); 151 | 152 | resource.write().then(() => { 153 | res.send({ 154 | id: 1, 155 | slug: "slug", 156 | status: "success", 157 | uid: resource.uid(), 158 | url: `/d/${resource.uid()}/slug`, 159 | version: 1, 160 | }); 161 | }).catch((err) => { 162 | console.error("Error writing file:", err); 163 | res.sendStatus(500); 164 | }); 165 | }, 166 | ); 167 | 168 | app.get( 169 | "/api/access-control/user/actions", 170 | express.json(), 171 | cors(corsOptions), 172 | (_: Request, res: Response) => { 173 | res.send({ 174 | /* eslint-disable-next-line @typescript-eslint/naming-convention */ 175 | "dashboards:write": true, 176 | }); 177 | return; 178 | }, 179 | ); 180 | 181 | const mustProxyGET = [ 182 | "/public/*", 183 | "/api/datasources/proxy/*", 184 | "/api/datasources/*", 185 | "/api/library-elements*", 186 | "/api/plugins/*", 187 | "/avatar/*", 188 | ]; 189 | for (const path of mustProxyGET) { 190 | app.get(path, function (req: Request, res: Response) { 191 | proxy.web(req, res, {}); 192 | }); 193 | } 194 | 195 | const mustProxyPOST = [ 196 | "/api/ds/query", 197 | "/api/datasources/proxy/*", 198 | "/api/datasources/uid/*", 199 | ]; 200 | for (const path of mustProxyPOST) { 201 | app.post(path, function (req: Request, res: Response) { 202 | proxy.web(req, res, {}); 203 | }); 204 | } 205 | 206 | const blockJSONget: { [name: string]: any } = { 207 | /* eslint-disable @typescript-eslint/naming-convention */ 208 | "/api/ma/events": [], 209 | "/api/live/publish": [], 210 | "/api/live/list": [], 211 | "/api/user/orgs": [], 212 | "/api/annotations": [], 213 | "/api/search": [], 214 | "/api/usage/*": [], 215 | "/api/org/preferences": [], 216 | "/api/prometheus/grafana/api/v1/rules": { 217 | status: "success", 218 | data: { groups: [] }, 219 | }, 220 | "/api/folders": [], 221 | "/api/ruler/grafana/api/v1/rules": {}, 222 | "/api/recording-rules": [], 223 | "/api/recording-rules/writer": { 224 | "id": "cojWep7Vz", 225 | "data_source_uid": "grafanacloud-prom", 226 | "remote_write_path": "/api/prom/push" 227 | }, 228 | "/apis/banners.grafana.app/*": {}, 229 | "/api/user/preferences": {}, 230 | /* eslint-enable @typescript-eslint/naming-convention */ 231 | }; 232 | for (const path in blockJSONget) { 233 | app.get(path, function (req: Request, res: Response) { 234 | res.send(blockJSONget[path]); 235 | }); 236 | } 237 | 238 | const blockJSONpost: { [name: string]: any } = { 239 | /* eslint-disable @typescript-eslint/naming-convention */ 240 | "/api/frontend-metrics": [], 241 | "/api/search-v2": [], 242 | "/api/live/publish": {}, 243 | /* eslint-enable @typescript-eslint/naming-convention */ 244 | }; 245 | for (const path in blockJSONpost) { 246 | app.post(path, function (req: Request, res: Response) { 247 | res.send(blockJSONpost[path]); 248 | }); 249 | } 250 | 251 | server.listen(port, () => { 252 | //@ts-expect-error 253 | port = server?.address()?.port; 254 | console.log("Server started"); 255 | }); 256 | } 257 | 258 | export function restartServer(secrets: vscode.SecretStorage, extensionPath: string) { 259 | console.log("Restarting server"); 260 | stopServer(); 261 | startServer(secrets, extensionPath); 262 | } 263 | export function stopServer() { 264 | server?.close(); 265 | } 266 | -------------------------------------------------------------------------------- /src/telemetry.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import vscode from 'vscode'; 3 | import * as util from './util'; 4 | 5 | const LAST_UPDATED_DATE = "lastUpdatedDate"; 6 | const INSTALLATION_DATE = "installDate"; 7 | const INSTALLATION_UUID = "installUUID"; 8 | const RECENT_VIEWS = "recentViews"; 9 | 10 | const URL = "https://stats.grafana.org/vscode-usage-report"; 11 | 12 | /* 13 | * Sends a single anonymous telemetry call once per day, allowing tracking of 14 | * usage - reports on first opening of a dashboard each day. 15 | */ 16 | export async function sendTelemetry(ctx: vscode.ExtensionContext) { 17 | const settings = vscode.workspace.getConfiguration("grafana-vscode"); 18 | const enableTelemetry = settings.get("telemetry"); 19 | if (!enableTelemetry) { 20 | return; 21 | } 22 | const lastUpdatedDate = ctx.globalState.get(LAST_UPDATED_DATE); 23 | const today = new Date(); 24 | 25 | if (lastUpdatedDate === undefined) { 26 | const uuid = uuidv4(); 27 | await sendEvent("first", uuid, today.toISOString(), 1); 28 | ctx.globalState.update(LAST_UPDATED_DATE, today); 29 | ctx.globalState.update(INSTALLATION_UUID, uuid); 30 | ctx.globalState.update(INSTALLATION_DATE, today); 31 | ctx.globalState.update(RECENT_VIEWS, 0); 32 | } else { 33 | let recentViews = ctx.globalState.get(RECENT_VIEWS); 34 | recentViews = (recentViews === undefined) ? 1 : recentViews + 1; 35 | 36 | if (differentDay(new Date(lastUpdatedDate), today)) { 37 | let uuid = ctx.globalState.get(INSTALLATION_UUID); 38 | let installDate = ctx.globalState.get(INSTALLATION_DATE); 39 | if (uuid === undefined) { 40 | console.log("UUID undefined. Shouldn't happen."); 41 | uuid = uuidv4(); 42 | ctx.globalState.update(INSTALLATION_UUID, uuid); 43 | } 44 | if (installDate === undefined) { 45 | console.log("Install date undefined. Shouldn't happen."); 46 | installDate = (new Date(lastUpdatedDate)).toISOString(); 47 | ctx.globalState.update(INSTALLATION_DATE, installDate); 48 | } 49 | await sendEvent("subsequent", uuid as string, installDate as string, recentViews); 50 | ctx.globalState.update(LAST_UPDATED_DATE, today); 51 | recentViews = 0; 52 | } 53 | ctx.globalState.update(RECENT_VIEWS, recentViews); 54 | } 55 | } 56 | 57 | function differentDay(d1: Date, d2: Date) { 58 | return d1.getDate() !== d2.getDate() || 59 | d1.getMonth() !== d2.getMonth() || 60 | d1.getFullYear() !== d2.getFullYear(); 61 | } 62 | 63 | async function sendEvent(eventType: string, uuid: string, installDate: string, views: number | undefined) { 64 | const data = { 65 | uuid: uuid, 66 | eventType: eventType, 67 | timestamp: Date(), 68 | createdAt: installDate, 69 | os: process.platform, 70 | arch: process.arch, 71 | packaging: "unknown", 72 | views: views, 73 | version: util.getVersion(), 74 | }; 75 | 76 | try { 77 | await fetch(URL, { 78 | method: 'post', 79 | body: JSON.stringify(data), 80 | headers: { 81 | // eslint-disable-next-line @typescript-eslint/naming-convention 82 | 'User-Agent': util.getUserAgent(), 83 | }, 84 | }); 85 | } catch (e) { 86 | console.log("Telemetry error", e, "for event", eventType); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { runTests } from "@vscode/test-electron"; 3 | 4 | async function main() { 5 | try { 6 | // The folder containing the Extension Manifest package.json 7 | // Passed to `--extensionDevelopmentPath` 8 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 9 | 10 | // The path to test runner 11 | // Passed to --extensionTestsPath 12 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 13 | 14 | // Download VS Code, unzip it and run the integration test 15 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 16 | } catch (err) { 17 | console.error("Failed to run tests", err); 18 | process.exit(1); 19 | } 20 | } 21 | 22 | main(); 23 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import vscode from "vscode"; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | test("Sample test", () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import Mocha from "mocha"; 3 | import glob from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise((c, e) => { 15 | glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run((failures) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | let userAgent: string; 2 | let version: string; 3 | 4 | export function setVersion(v: string) { 5 | version = v; 6 | userAgent = `Grafana VSCode Extension/v${version}`; 7 | } 8 | 9 | export function getVersion(): string { 10 | return version; 11 | } 12 | 13 | export function getUserAgent(): string { 14 | return userAgent; 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2021", 5 | "esModuleInterop": true, 6 | "lib": ["ES2021"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */ 10 | /* Additional Checks */ 11 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | "use strict"; 4 | 5 | const path = require("path"); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: "node", // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, "dist"), 19 | filename: "extension.js", 20 | libraryTarget: "commonjs2", 21 | }, 22 | externals: { 23 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: [".ts", ".js"], 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: "ts-loader", 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | devtool: "nosources-source-map", 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [extensionConfig]; 49 | --------------------------------------------------------------------------------