├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── a_question.md │ ├── bug_report.md │ ├── enhancement.md │ └── task.md └── workflows │ ├── ci.yml │ ├── docker.yml │ └── main.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── BUILD.md ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── config └── jest-setup.ts ├── docker-compose.yml ├── docker-compose ├── dev.yml └── master.yml ├── jest.config.js ├── package.json ├── provisioning ├── datasources │ └── redis.yaml └── plugins │ └── redis-app.yaml ├── src ├── components │ ├── Config │ │ ├── Config.test.tsx │ │ ├── Config.tsx │ │ └── index.ts │ ├── DataSourceList │ │ ├── DataSourceList.test.tsx │ │ ├── DataSourceList.tsx │ │ └── index.ts │ ├── RootPage │ │ ├── RootPage.test.tsx │ │ ├── RootPage.tsx │ │ └── index.ts │ └── index.ts ├── constants.ts ├── dashboards │ ├── redis-cli.json │ ├── redis-gears.json │ └── redis-overview.json ├── icons │ ├── HighAvailability.tsx │ ├── MultiLayerSecurity.tsx │ ├── RediSearch.tsx │ ├── RedisAI.tsx │ ├── RedisBloom.tsx │ ├── RedisCube.tsx │ ├── RedisGears.tsx │ ├── RedisGraph.tsx │ ├── RedisJSON.tsx │ ├── RedisTimeSeries.tsx │ └── index.ts ├── img │ ├── enable.png │ ├── grafana-marketplace.png │ ├── logo.svg │ ├── redis-app.png │ ├── redis-cli-dashboard.png │ ├── redis-cli-panel.png │ ├── redis-cpu-usage-graph.png │ ├── redis-gears-dashboard.png │ ├── redis-keys-panel.png │ ├── redis-latency-panel-graph.png │ └── redis-latency-panel-table.png ├── module.test.ts ├── module.ts ├── plugin.json ├── redis-cli-panel │ ├── components │ │ ├── AutoScrollingTextArea │ │ │ ├── AutoScrollingTextArea.test.tsx │ │ │ ├── AutoScrollingTextArea.tsx │ │ │ └── index.ts │ │ ├── RedisCLIPanel │ │ │ ├── RedisCLIPanel.test.tsx │ │ │ ├── RedisCLIPanel.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── constants.ts │ ├── help │ │ ├── index.ts │ │ ├── redis-ai.ts │ │ ├── redis-bloom.ts │ │ ├── redis-gears.ts │ │ ├── redis-graph.ts │ │ ├── redis-json.ts │ │ ├── redis-search.ts │ │ ├── redis-time-series.ts │ │ └── redis.ts │ ├── img │ │ └── logo.svg │ ├── module.test.ts │ ├── module.ts │ ├── plugin.json │ ├── styles.ts │ └── types.ts ├── redis-cpu-panel │ ├── components │ │ ├── RedisCPUGraph │ │ │ ├── RedisCPUGraph.test.tsx │ │ │ ├── RedisCPUGraph.tsx │ │ │ └── index.ts │ │ ├── RedisCPUPanel │ │ │ ├── RedisCPUPanel.test.tsx │ │ │ ├── RedisCPUPanel.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── constants.ts │ ├── img │ │ └── logo.svg │ ├── module.test.ts │ ├── module.ts │ ├── plugin.json │ └── types.ts ├── redis-gears-panel │ ├── components │ │ ├── CodeEditor │ │ │ ├── CodeEditor.test.tsx │ │ │ ├── CodeEditor.tsx │ │ │ └── index.ts │ │ ├── RedisGearsPanel │ │ │ ├── RedisGearsPanel.test.tsx │ │ │ ├── RedisGearsPanel.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── constants.ts │ ├── img │ │ └── logo.svg │ ├── module.test.ts │ ├── module.ts │ ├── plugin.json │ └── types.ts ├── redis-keys-panel │ ├── components │ │ ├── RedisKeysPanel │ │ │ ├── RedisKeysPanel.test.tsx │ │ │ ├── RedisKeysPanel.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── constants.ts │ ├── img │ │ └── logo.svg │ ├── module.test.ts │ ├── module.ts │ ├── plugin.json │ └── types.ts ├── redis-latency-panel │ ├── components │ │ ├── RedisLatencyGraph │ │ │ ├── RedisLatencyGraph.test.tsx │ │ │ ├── RedisLatencyGraph.tsx │ │ │ └── index.ts │ │ ├── RedisLatencyPanel │ │ │ ├── RedisLatencyPanel.test.tsx │ │ │ ├── RedisLatencyPanel.tsx │ │ │ └── index.ts │ │ ├── RedisLatencyTable │ │ │ ├── RedisLatencyTable.test.tsx │ │ │ ├── RedisLatencyTable.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── constants.ts │ ├── img │ │ └── logo.svg │ ├── module.test.ts │ ├── module.ts │ ├── plugin.json │ └── types.ts └── types.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | config 3 | coverage 4 | docker-compose 5 | node_modules 6 | src -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 120 11 | 12 | [*.{js,ts,tsx,scss}] 13 | quote_type = single 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/a_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 🤔 3 | about: Usage question or discussion about Redis Application plugin for Grafana. 4 | title: '' 5 | labels: 'kind/question' 6 | assignees: '' 7 | 8 | --- 9 | ## Summary 10 | 11 | 12 | ## Relevant information 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐞 3 | about: Report a bug 4 | title: '' 5 | labels: 'kind/bug' 6 | assignees: '' 7 | 8 | --- 9 | ### Describe the bug 10 | 11 | 12 | ### Version 13 | 14 | 15 | ### Steps to reproduce 16 | 21 | 22 | ### Expected behavior 23 | 24 | 25 | ### Screenshots 26 | 27 | 28 | ### Additional context 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement💡 3 | about: Suggest a enhancement 4 | title: '' 5 | labels: 'kind/enhancement' 6 | assignees: '' 7 | 8 | --- 9 | ### Is your enhancement related to a problem? Please describe. 10 | 11 | 12 | ### Describe the solution you'd like 13 | 14 | 15 | ### Describe alternatives you've considered 16 | 17 | 18 | ### Additional context 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 🔧 3 | about: Internal things, technical debt, and to-do tasks to be performed. 4 | title: '' 5 | labels: 'kind/task' 6 | assignees: '' 7 | 8 | --- 9 | ### Is your task related to a problem? Please describe. 10 | 11 | 12 | ### Describe the solution you'd like 13 | 14 | 15 | ### Describe alternatives you've considered 16 | 17 | 18 | ### Additional context 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Setup Node.js environment 13 | uses: actions/setup-node@v2.1.2 14 | with: 15 | node-version: "16.x" 16 | 17 | - name: Get yarn cache directory path 18 | id: yarn-cache-dir-path 19 | run: echo "::set-output name=dir::$(yarn cache dir)" 20 | 21 | - name: Cache yarn cache 22 | uses: actions/cache@v2 23 | id: cache-yarn-cache 24 | with: 25 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | 30 | - name: Cache node_modules 31 | id: cache-node-modules 32 | uses: actions/cache@v2 33 | with: 34 | path: node_modules 35 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- 38 | 39 | - name: Install dependencies 40 | run: yarn install --frozen-lockfile; 41 | if: | 42 | steps.cache-yarn-cache.outputs.cache-hit != 'true' || 43 | steps.cache-node-modules.outputs.cache-hit != 'true' 44 | 45 | - name: Build and test frontend 46 | run: yarn build 47 | 48 | - name: Upload coverage to Codecov 49 | uses: codecov/codecov-action@v2 50 | with: 51 | directory: ./coverage/ 52 | env_vars: OS,PYTHON 53 | fail_ci_if_error: true 54 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | schedule: 5 | - cron: '0 1 * * *' # run at 1 AM UTC 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | grafana-redis-datasource: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | repository: RedisGrafana/grafana-redis-datasource 17 | 18 | - name: Setup Node.js environment 19 | uses: actions/setup-node@v2.1.2 20 | with: 21 | node-version: "16.x" 22 | 23 | - name: Get yarn cache directory path 24 | id: yarn-cache-dir-path 25 | run: echo "::set-output name=dir::$(yarn cache dir)" 26 | 27 | - name: Cache yarn cache 28 | uses: actions/cache@v2 29 | id: cache-yarn-cache 30 | with: 31 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 32 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-yarn- 35 | 36 | - name: Cache node_modules 37 | id: cache-node-modules 38 | uses: actions/cache@v2 39 | with: 40 | path: node_modules 41 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} 42 | restore-keys: | 43 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- 44 | 45 | - name: Install dependencies 46 | run: yarn install --frozen-lockfile; 47 | if: | 48 | steps.cache-yarn-cache.outputs.cache-hit != 'true' || 49 | steps.cache-node-modules.outputs.cache-hit != 'true' 50 | 51 | - name: Build and test frontend 52 | run: yarn build 53 | 54 | - name: Check for backend 55 | id: check-for-backend 56 | run: | 57 | if [ -f "Magefile.go" ] 58 | then 59 | echo "::set-output name=has-backend::true" 60 | fi 61 | 62 | - name: Setup Go environment 63 | if: steps.check-for-backend.outputs.has-backend == 'true' 64 | uses: actions/setup-go@v2 65 | with: 66 | go-version: "1.16" 67 | 68 | - name: Test backend 69 | if: steps.check-for-backend.outputs.has-backend == 'true' 70 | uses: magefile/mage-action@v1 71 | with: 72 | version: latest 73 | args: cover 74 | 75 | - name: Build backend 76 | if: steps.check-for-backend.outputs.has-backend == 'true' 77 | uses: magefile/mage-action@v1 78 | with: 79 | version: latest 80 | args: buildAll 81 | 82 | - name: Sign plugin 83 | run: yarn sign 84 | env: 85 | GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com. 86 | 87 | - name: Upload artifact 88 | uses: actions/upload-artifact@v2 89 | with: 90 | name: grafana-redis-datasource 91 | path: ./dist 92 | 93 | grafana-redis-app: 94 | needs: 95 | - grafana-redis-datasource 96 | runs-on: ubuntu-latest 97 | 98 | steps: 99 | - uses: actions/checkout@v2 100 | with: 101 | repository: RedisGrafana/grafana-redis-app 102 | 103 | - name: Setup Node.js environment 104 | uses: actions/setup-node@v2.1.2 105 | with: 106 | node-version: "14.x" 107 | 108 | - name: Get yarn cache directory path 109 | id: yarn-cache-dir-path 110 | run: echo "::set-output name=dir::$(yarn cache dir)" 111 | 112 | - name: Cache yarn cache 113 | uses: actions/cache@v2 114 | id: cache-yarn-cache 115 | with: 116 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 117 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 118 | restore-keys: | 119 | ${{ runner.os }}-yarn- 120 | 121 | - name: Cache node_modules 122 | id: cache-node-modules 123 | uses: actions/cache@v2 124 | with: 125 | path: node_modules 126 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} 127 | restore-keys: | 128 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- 129 | 130 | - name: Install dependencies 131 | run: yarn install --frozen-lockfile; 132 | if: | 133 | steps.cache-yarn-cache.outputs.cache-hit != 'true' || 134 | steps.cache-node-modules.outputs.cache-hit != 'true' 135 | 136 | - name: Build and test frontend 137 | run: yarn build 138 | 139 | - name: Sign plugin 140 | run: yarn run grafana-toolkit plugin:sign 141 | env: 142 | GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com. 143 | 144 | - name: Download artifact 145 | uses: actions/download-artifact@v2 146 | with: 147 | name: grafana-redis-datasource 148 | path: redis-datasource 149 | 150 | - name: Set up QEMU 151 | uses: docker/setup-qemu-action@v1 152 | 153 | - name: Set up Docker Buildx 154 | uses: docker/setup-buildx-action@v1 155 | 156 | - name: Cache Docker layers 157 | uses: actions/cache@v2 158 | with: 159 | path: /tmp/.buildx-cache 160 | key: ${{ runner.os }}-buildx-${{ github.sha }} 161 | restore-keys: | 162 | ${{ runner.os }}-buildx- 163 | 164 | - name: Login to GitHub Container Registry 165 | uses: docker/login-action@v1 166 | with: 167 | registry: ghcr.io 168 | username: ${{ github.repository_owner }} 169 | password: ${{ secrets.GITHUB_TOKEN }} 170 | 171 | - name: Build and push master 172 | id: docker_build_master 173 | uses: docker/build-push-action@v2 174 | with: 175 | context: . 176 | file: ./Dockerfile 177 | platforms: linux/amd64,linux/arm64,linux/arm 178 | push: true 179 | build-args: GRAFANA_VERSION=master 180 | tags: ghcr.io/redisgrafana/redis-app:master 181 | cache-from: type=local,src=/tmp/.buildx-cache 182 | cache-to: type=local,dest=/tmp/.buildx-cache 183 | 184 | - name: Build and push latest 185 | id: docker_build 186 | uses: docker/build-push-action@v2 187 | with: 188 | context: . 189 | file: ./Dockerfile 190 | platforms: linux/amd64,linux/arm64,linux/arm 191 | push: true 192 | tags: ghcr.io/redisgrafana/redis-app:latest 193 | cache-from: type=local,src=/tmp/.buildx-cache 194 | cache-to: type=local,dest=/tmp/.buildx-cache 195 | 196 | - name: Image digest 197 | run: echo ${{ steps.docker_build.outputs.digest }} 198 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" # Run workflow on version tags, e.g. v1.0.0. 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v2.1.2 17 | with: 18 | node-version: "16.x" 19 | 20 | - name: Get yarn cache directory path 21 | id: yarn-cache-dir-path 22 | run: echo "::set-output name=dir::$(yarn cache dir)" 23 | 24 | - name: Cache yarn cache 25 | uses: actions/cache@v2 26 | id: cache-yarn-cache 27 | with: 28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-yarn- 32 | 33 | - name: Cache node_modules 34 | id: cache-node-modules 35 | uses: actions/cache@v2 36 | with: 37 | path: node_modules 38 | key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-${{ matrix.node-version }}-nodemodules- 41 | 42 | - name: Install dependencies 43 | run: yarn install --frozen-lockfile; 44 | if: | 45 | steps.cache-yarn-cache.outputs.cache-hit != 'true' || 46 | steps.cache-node-modules.outputs.cache-hit != 'true' 47 | 48 | - name: Build and test frontend 49 | run: yarn build 50 | 51 | - name: Check for backend 52 | id: check-for-backend 53 | run: | 54 | if [ -f "Magefile.go" ] 55 | then 56 | echo "::set-output name=has-backend::true" 57 | fi 58 | 59 | - name: Setup Go environment 60 | uses: actions/setup-go@v2 61 | with: 62 | go-version: "1.16" 63 | 64 | - name: Test backend 65 | if: steps.check-for-backend.outputs.has-backend == 'true' 66 | uses: magefile/mage-action@v1 67 | with: 68 | version: latest 69 | args: coverage 70 | 71 | - name: Build backend 72 | if: steps.check-for-backend.outputs.has-backend == 'true' 73 | uses: magefile/mage-action@v1 74 | with: 75 | version: latest 76 | args: buildAll 77 | 78 | - name: Sign plugin 79 | run: yarn sign 80 | env: 81 | GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com. 82 | 83 | - name: Get plugin metadata 84 | id: metadata 85 | run: | 86 | sudo apt-get install jq 87 | 88 | export GRAFANA_PLUGIN_ID=$(cat dist/plugin.json | jq -r .id) 89 | export GRAFANA_PLUGIN_VERSION=$(cat dist/plugin.json | jq -r .info.version) 90 | export GRAFANA_PLUGIN_TYPE=$(cat dist/plugin.json | jq -r .type) 91 | export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip 92 | export GRAFANA_PLUGIN_ARTIFACT_CHECKSUM=${GRAFANA_PLUGIN_ARTIFACT}.md5 93 | 94 | echo "::set-output name=plugin-id::${GRAFANA_PLUGIN_ID}" 95 | echo "::set-output name=plugin-version::${GRAFANA_PLUGIN_VERSION}" 96 | echo "::set-output name=plugin-type::${GRAFANA_PLUGIN_TYPE}" 97 | echo "::set-output name=archive::${GRAFANA_PLUGIN_ARTIFACT}" 98 | echo "::set-output name=archive-checksum::${GRAFANA_PLUGIN_ARTIFACT_CHECKSUM}" 99 | 100 | echo ::set-output name=github-tag::${GITHUB_REF#refs/*/} 101 | 102 | - name: Read changelog 103 | id: changelog 104 | run: | 105 | awk '/^## / {s++} s == 1 {print}' CHANGELOG.md > release_notes.md 106 | echo "::set-output name=path::release_notes.md" 107 | 108 | - name: Check package version 109 | run: if [ "v${{ steps.metadata.outputs.plugin-version }}" != "${{ steps.metadata.outputs.github-tag }}" ]; then printf "\033[0;31mPlugin version doesn't match tag name\033[0m\n"; exit 1; fi 110 | 111 | - name: Package plugin 112 | id: package-plugin 113 | run: | 114 | mv dist ${{ steps.metadata.outputs.plugin-id }} 115 | zip ${{ steps.metadata.outputs.archive }} ${{ steps.metadata.outputs.plugin-id }} -r 116 | md5sum ${{ steps.metadata.outputs.archive }} > ${{ steps.metadata.outputs.archive-checksum }} 117 | echo "::set-output name=checksum::$(cat ./${{ steps.metadata.outputs.archive-checksum }} | cut -d' ' -f1)" 118 | 119 | - name: Lint plugin 120 | continue-on-error: true 121 | run: | 122 | git clone https://github.com/grafana/plugin-validator 123 | pushd ./plugin-validator/pkg/cmd/plugincheck 124 | go install 125 | popd 126 | plugincheck ${{ steps.metadata.outputs.archive }} 127 | 128 | - name: Create release 129 | id: create_release 130 | uses: actions/create-release@v1 131 | env: 132 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 133 | with: 134 | tag_name: ${{ github.ref }} 135 | release_name: Release ${{ github.ref }} 136 | body_path: ${{ steps.changelog.outputs.path }} 137 | draft: true 138 | 139 | - name: Add plugin to release 140 | id: upload-plugin-asset 141 | uses: actions/upload-release-asset@v1 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | with: 145 | upload_url: ${{ steps.create_release.outputs.upload_url }} 146 | asset_path: ./${{ steps.metadata.outputs.archive }} 147 | asset_name: ${{ steps.metadata.outputs.archive }} 148 | asset_content_type: application/zip 149 | 150 | - name: Add checksum to release 151 | id: upload-checksum-asset 152 | uses: actions/upload-release-asset@v1 153 | env: 154 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 155 | with: 156 | upload_url: ${{ steps.create_release.outputs.upload_url }} 157 | asset_path: ./${{ steps.metadata.outputs.archive-checksum }} 158 | asset_name: ${{ steps.metadata.outputs.archive-checksum }} 159 | asset_content_type: text/plain 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | node_modules/ 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Compiled binary addons (https://nodejs.org/api/addons.html) 23 | dist/ 24 | redis-datasource 25 | artifacts/ 26 | work/ 27 | ci/ 28 | e2e-results/ 29 | 30 | # Editors 31 | .DS_Store 32 | .idea 33 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gitkeep 3 | .gitattributes 4 | .gitignore 5 | .editorconfig 6 | .prettierignore 7 | LICENSE 8 | yarn.lock 9 | .github 10 | Dockerfile 11 | .dockerignore 12 | 13 | # Folders 14 | node_modules 15 | coverage 16 | data 17 | dist 18 | redis-datasource 19 | img 20 | .idea 21 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: false, 3 | tabWidth: 2, 4 | semi: true, 5 | bracketSpacing: true, 6 | arrowParens: 'always', 7 | ...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'), 8 | }; 9 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # How to build and install Redis Application plugin 2 | 3 | You can find detailed instructions in the [Documentation](https://redisgrafana.github.io/development/redis-app/). 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.2.1 (2022-01-18) 4 | 5 | ### Features / Enhancements 6 | 7 | - Rebuild using 8.3.4 (#99) 8 | 9 | ## 2.2.0 (2022-01-17) 10 | 11 | ### Features / Enhancements 12 | 13 | - Upgrade to Grafana 8.2.5 (#88) 14 | - Add CLUSTER Slots commands introduces in Redis 7 (#89) 15 | - Fix Docker plugins provisioning (#90) 16 | - Upgrade to Grafana 8.3.0 (#93) 17 | - Fix LGTM and Update Panel Options (#95) 18 | - Add Redis CPU Usage panel (#96) 19 | - Update Components naming (#97) 20 | - Add CPU Usage Stacking and Gradient (#98) 21 | - Update "Can't retrieve a list of commands" error message (#100) 22 | 23 | ## 2.1.0 (2021-11-10) 24 | 25 | ### Features / Enhancements 26 | 27 | - Update to Grafana 8.0.6 (#78) 28 | - Update to Grafana 8.1.1 (#79) 29 | - Add CLI commands for Redis 7.0.0 (#80) 30 | - Update to Grafana 8.1.4 (#81) 31 | - Add new command for Redis 6.2, Redis 7.0 (#82) 32 | - Add demo at https://demo.volkovlabs.io (#83) 33 | - Update Redis 7.0 commands help (#85) 34 | - Update to Grafana 8.2.3 (#86) 35 | - CLI Panel respects disabled CLI option for Data Source (#87) 36 | 37 | ## 2.0.1 (2021-07-07) 38 | 39 | ### Features / Enhancements 40 | 41 | - Update Redis Data Source dependency to 1.5.0 (#77) 42 | 43 | ## 2.0.0 (2021-06-25) 44 | 45 | ### Breaking changes 46 | 47 | Supports Grafana 8.0+, for Grafana 7.X use version 1.2.0 48 | 49 | ### Features / Enhancements 50 | 51 | - Upgrade to Grafana 8.0.2 (#67) 52 | - Update dashboards for v8 and minor updates (#69) 53 | - Replace old Latency Graph with TimeSeries component (#70) 54 | - Add Redis 7 commands to CLI (#71) 55 | - Add NgAlert and Plugin catalog to docker image (#72) 56 | - Update dashboards in the application's menu as pages (#73) 57 | - Update CLI legacy switch to ButtonGroup (#74) 58 | 59 | ### Bug fixes 60 | 61 | - "Available Requirements" panel should be set to $redis datasource #63 62 | - Cannot read property 'v1' of undefined (theme.v1) in the Grafana 8 #65 63 | - Add theme to getDisplayProcessor (#66) 64 | - Fix adding new data source and minor updates (#68) 65 | 66 | ## 1.2.0 (2021-05-11) 67 | 68 | ### Features / Enhancements 69 | 70 | - Upgrade Grafana dependencies 7.5.4 #53 71 | - Update Docker workflow #52 72 | - Update Docker token and add Master build #54 73 | - Add RefId to Query as mandatory for the upcoming release #55 74 | - Update RedisGears Script Editor Execution modes #57 75 | - Update Dashboards to version 7.5.5 #58 76 | 77 | ### Bug fixes 78 | 79 | - Add default Color Mode Id for older releases (7.2.X) #49 80 | - Fix Latency below zero calculation #56 81 | - "Cannot read property 'Tooltip' of undefined" for Latency Panel in the upcoming release #60 82 | 83 | ## 1.1.0 (2021-02-07) 84 | 85 | ### Features / Enhancements 86 | 87 | - Make Application plugin's icon bolder for better visibility #19 88 | - Update Grafana dependencies to 7.3.6 #21 89 | - Update Angular legacy code to React #25 90 | - Update Redis CLI auto-scrolling textarea to autosize #26 91 | - Add Tests coverage #28 92 | - Create Latency Panel to display Latency Chart for each command #29 93 | - Improve CLI panel error handling and add new CLI/Raw mode switch #33 94 | - Improve Latency Panel to display Graph and set Data Source query #32 95 | - Create the panel to show the biggest keys #34 96 | - Create RedisGears panel #36 97 | - Improve data source list #38 98 | - Add Monaco for RedisGears panel editor #39 99 | - Update CLI Panel helpers for Redis 6.2 and modules #40 100 | - Add Docker build #42 101 | - Update Plugin and panels configuration for Redis Data Source 1.3.1 #44 102 | 103 | ## 1.0.1 (2020-10-24) 104 | 105 | ### Features / Enhancements 106 | 107 | - Add GitHub action to sign release #13 108 | - Signed release 109 | 110 | ## 1.0.0 (2020-10-14) 111 | 112 | ### Features / Enhancements 113 | 114 | - Initial release based on Grafana 7.2.0 and Redis Data Source 1.2.0. 115 | - Allows seeing all Redis Data Sources with supported modules. 116 | - Provides Redis CLI Panel with hints for Redis and various modules commands. 117 | - Includes Redis Overview and Redis CLI dashboards. 118 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GRAFANA_VERSION="latest" 2 | 3 | FROM grafana/grafana:${GRAFANA_VERSION}-ubuntu 4 | 5 | # Set DEBIAN_FRONTEND=noninteractive in environment at build-time 6 | ARG DEBIAN_FRONTEND=noninteractive 7 | 8 | # Set Grafana options 9 | ENV GF_ENABLE_GZIP="true" 10 | ENV GF_USERS_DEFAULT_THEME="light" 11 | 12 | # Paths 13 | ENV GF_PATHS_PROVISIONING="/etc/grafana/provisioning" 14 | ENV GF_PATHS_PLUGINS="/var/lib/grafana/plugins" 15 | 16 | # Copy artifacts 17 | COPY dist $GF_PATHS_PLUGINS/redis-app 18 | COPY redis-datasource $GF_PATHS_PLUGINS/redis-datasource 19 | 20 | # Provisioning 21 | COPY provisioning/plugins $GF_PATHS_PROVISIONING/plugins 22 | 23 | # Add Execute permissions 24 | USER root 25 | RUN chmod +x $GF_PATHS_PLUGINS/redis-datasource/redis-datasource* 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Application plugin for Grafana 2 | 3 | ![Application](https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/master/src/img/redis-app.png) 4 | 5 | [![Grafana 8](https://img.shields.io/badge/Grafana-8-orange)](https://www.grafana.com) 6 | [![Redis Data Source](https://img.shields.io/badge/dynamic/json?color=blue&label=Redis%20Data%20Source&query=%24.version&url=https%3A%2F%2Fgrafana.com%2Fapi%2Fplugins%2Fredis-datasource)](https://grafana.com/grafana/plugins/redis-datasource) 7 | [![Redis Application plugin](https://img.shields.io/badge/dynamic/json?color=blue&label=Redis%20Application%20plugin&query=%24.version&url=https%3A%2F%2Fgrafana.com%2Fapi%2Fplugins%2Fredis-app)](https://grafana.com/grafana/plugins/redis-app) 8 | ![CI](https://github.com/RedisGrafana/grafana-redis-app/workflows/CI/badge.svg) 9 | ![Docker](https://github.com/RedisGrafana/grafana-redis-app/workflows/Docker/badge.svg) 10 | [![codecov](https://codecov.io/gh/RedisGrafana/grafana-redis-app/branch/master/graph/badge.svg?token=15SIRGU8SX)](https://codecov.io/gh/RedisGrafana/grafana-redis-app) 11 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/RedisGrafana/grafana-redis-app.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/RedisGrafana/grafana-redis-app/context:javascript) 12 | 13 | ## Introduction 14 | 15 | The Redis Application is a plugin for Grafana that provides application pages, custom panels, and dashboards for [Redis Data Source](https://grafana.com/grafana/plugins/redis-datasource). 16 | 17 | ### Custom Panels 18 | 19 | - [Command-line interface (CLI)](https://redisgrafana.github.io/redis-app/panels/redis-cli-panel/) 20 | - [Command Latency (graph and table)](https://redisgrafana.github.io/redis-app/panels/redis-latency-panel/) 21 | - [Keys consuming a lot of memory](https://redisgrafana.github.io/redis-app/panels/redis-keys-panel/) 22 | - [RedisGears Script Editor](https://redisgrafana.github.io/redis-app/panels/redis-gears-panel/) 23 | - [CPU Usage](https://redisgrafana.github.io/redis-app/panels/redis-cpu-panel/) 24 | 25 | ### Dashboards 26 | 27 | - [Redis CLI](https://redisgrafana.github.io/redis-app/dashboards/cli/) 28 | - [Redis Overview](https://redisgrafana.github.io/redis-app/dashboards/overview/) 29 | - [RedisGears](https://redisgrafana.github.io/redis-app/dashboards/redis-gears/) 30 | 31 | All dashboards are available from the application's icon in the left side menu. 32 | 33 | ![Redis-CLI-Dashboards](https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/master/src/img/redis-cli-dashboard.png) 34 | 35 | ### Demo 36 | 37 | Demo is available on [demo.volkovlabs.io](https://demo.volkovlabs.io): 38 | 39 | - [Redis Overview dashboard](https://demo.volkovlabs.io/d/TgibHBv7z/redis-overview?orgId=1&refresh=1h) 40 | - [Projects](https://demo.volkovlabs.io) 41 | 42 | ### Requirements 43 | 44 | - **Grafana 8.0+** is required for Redis Application 2.X. 45 | - **Grafana 7.1+** is required for Redis Application 1.X. 46 | 47 | ## Getting Started 48 | 49 | Redis Application plugin can be installed from the Grafana Marketplace or use the `grafana-cli` tool to install from the command line: 50 | 51 | ```bash 52 | grafana-cli plugins install redis-app 53 | ``` 54 | 55 | ![Grafana-Marketplace](https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/master/src/img/grafana-marketplace.png) 56 | 57 | For Docker instructions and installation without Internet access, follow the [Quickstart](https://redisgrafana.github.io/quickstart/) page. 58 | 59 | ### Open Grafana and enable Redis Application plugin 60 | 61 | Open Grafana in your browser, enable Redis Application plugin, and configure Redis Data Sources. 62 | 63 | ![Enable](https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/master/src/img/enable.png) 64 | 65 | ## Documentation 66 | 67 | Take a look at the [Documentation](https://redisgrafana.github.io/redis-app/overview/) to learn more about the Redis Application plugin, Redis Data Source, provided dashboards, and custom panels. 68 | 69 | ## Development 70 | 71 | [Developing Redis Application plugin](https://redisgrafana.github.io/development/redis-app/) page provides instructions on building the application. 72 | 73 | Are you interested in the latest features and updates? Start nightly built [Docker image for Redis Application plugin](https://redisgrafana.github.io/development/images/). 74 | 75 | ## Feedback 76 | 77 | We love to hear from users, developers, and the whole community interested in this plugin. These are various ways to get in touch with us: 78 | 79 | - Ask a question, request a new feature, and file a bug with [GitHub issues](https://github.com/RedisGrafana/grafana-redis-app/issues/new/choose). 80 | - Star the repository to show your support. 81 | 82 | ## Contributing 83 | 84 | - Fork the repository. 85 | - Find an issue to work on and submit a pull request. 86 | - Could not find an issue? Look for documentation, bugs, typos, and missing features. 87 | 88 | ## License 89 | 90 | - Apache License Version 2.0, see [LICENSE](https://github.com/RedisGrafana/grafana-redis-app/blob/master/LICENSE). 91 | -------------------------------------------------------------------------------- /config/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | /** 5 | * Configure for React 6 | */ 7 | configure({ adapter: new Adapter() }); 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | redis: 5 | container_name: redismod 6 | image: redislabs/redismod:latest 7 | ports: 8 | - 6379:6379/tcp 9 | 10 | grafana: 11 | container_name: grafana 12 | image: grafana/grafana:latest 13 | ports: 14 | - 3000:3000/tcp 15 | environment: 16 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 17 | - GF_AUTH_ANONYMOUS_ENABLED=true 18 | - GF_AUTH_BASIC_ENABLED=false 19 | - GF_ENABLE_GZIP=true 20 | - GF_USERS_DEFAULT_THEME=light 21 | - GF_INSTALL_PLUGINS=redis-app 22 | volumes: 23 | - ./provisioning:/etc/grafana/provisioning 24 | # Uncomment to preserve Grafana configuration 25 | # - ./data:/var/lib/grafana 26 | -------------------------------------------------------------------------------- /docker-compose/dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | redis: 5 | container_name: redismod 6 | image: redislabs/redismod:latest 7 | ports: 8 | - 6379:6379/tcp 9 | # Uncomment and edit the local path in the following line to have 10 | # Redis' data persisted to the host's filesystem. 11 | # volumes: 12 | # - ./dump.rdb:/data/dump.rdb 13 | 14 | grafana: 15 | container_name: grafana 16 | image: grafana/grafana:latest 17 | ports: 18 | - 3000:3000/tcp 19 | environment: 20 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 21 | - GF_AUTH_ANONYMOUS_ENABLED=true 22 | - GF_AUTH_BASIC_ENABLED=false 23 | - GF_ENABLE_GZIP=true 24 | - GF_USERS_DEFAULT_THEME=light 25 | - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=redis-app,redis-datasource 26 | - GF_DEFAULT_APP_MODE=development 27 | # Uncomment to run in debug mode 28 | # - GF_LOG_LEVEL=debug 29 | volumes: 30 | # Redis Data Source should be cloned and built 31 | - ../../grafana-redis-datasource/dist:/var/lib/grafana/plugins/redis-datasource 32 | - ../dist:/var/lib/grafana/plugins/redis-app 33 | - ../provisioning:/etc/grafana/provisioning 34 | # Uncomment to preserve Grafana configuration 35 | # - ./data:/var/lib/grafana 36 | -------------------------------------------------------------------------------- /docker-compose/master.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | redis: 5 | container_name: redismod 6 | image: redislabs/redismod:latest 7 | ports: 8 | - 6379:6379/tcp 9 | # Uncomment and edit the local path in the following line to have 10 | # Redis' data persisted to the host's filesystem. 11 | # volumes: 12 | # - ./dump.rdb:/data/dump.rdb 13 | 14 | grafana: 15 | container_name: grafana 16 | image: grafana/grafana:master 17 | ports: 18 | - 3000:3000/tcp 19 | environment: 20 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 21 | - GF_AUTH_ANONYMOUS_ENABLED=true 22 | - GF_AUTH_BASIC_ENABLED=false 23 | - GF_ENABLE_GZIP=true 24 | - GF_USERS_DEFAULT_THEME=light 25 | - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=redis-app,redis-datasource 26 | - GF_DEFAULT_APP_MODE=development 27 | # Uncomment to run in debug mode 28 | # - GF_LOG_LEVEL=debug 29 | volumes: 30 | # Redis Data Source should be cloned and built 31 | - ../../grafana-redis-datasource/dist:/var/lib/grafana/plugins/redis-datasource 32 | - ../dist:/var/lib/grafana/plugins/redis-app 33 | - ../provisioning:/etc/grafana/provisioning 34 | # Uncomment to preserve Grafana configuration 35 | # - ./data:/var/lib/grafana 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // This file is needed because it is used by vscode and other tools that 2 | // call `jest` directly. However, unless you are doing anything special 3 | // do not edit this file 4 | 5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); 6 | 7 | // This process will use the same config that `yarn test` is using 8 | module.exports = { 9 | ...standard.jestConfig(), 10 | collectCoverage: true, 11 | coveragePathIgnorePatterns: ['src/icons'], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "RedisGrafana", 3 | "description": "Redis Application for Grafana", 4 | "devDependencies": { 5 | "@grafana/data": "8.3.4", 6 | "@grafana/runtime": "8.3.4", 7 | "@grafana/toolkit": "8.3.4", 8 | "@grafana/ui": "8.3.4", 9 | "@monaco-editor/react": "^4.3.1", 10 | "@types/enzyme": "^3.10.11", 11 | "@types/enzyme-adapter-react-16": "^1.0.6", 12 | "emotion": "11.0.0", 13 | "enzyme": "^3.11.0", 14 | "enzyme-adapter-react-16": "^1.15.6" 15 | }, 16 | "engines": { 17 | "node": ">=14" 18 | }, 19 | "license": "Apache-2.0", 20 | "name": "redis-app", 21 | "scripts": { 22 | "build": "grafana-toolkit plugin:build --coverage", 23 | "dev": "grafana-toolkit plugin:dev", 24 | "format": "prettier --write \"**\"", 25 | "sign": "grafana-toolkit plugin:sign", 26 | "start": "docker-compose up", 27 | "start:dev": "docker-compose -f docker-compose/dev.yml pull && docker-compose -f docker-compose/dev.yml up", 28 | "start:master": "docker-compose -f docker-compose/master.yml pull && docker-compose -f docker-compose/master.yml up", 29 | "stop": "docker-compose down", 30 | "stop:dev": "docker-compose -f docker-compose/dev.yml down", 31 | "test": "grafana-toolkit plugin:test --coverage", 32 | "upgrade": "yarn upgrade --latest", 33 | "watch": "grafana-toolkit plugin:dev --watch" 34 | }, 35 | "version": "2.2.1" 36 | } 37 | -------------------------------------------------------------------------------- /provisioning/datasources/redis.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Redis 5 | type: redis-datasource 6 | access: proxy 7 | orgId: 1 8 | isDefault: true 9 | version: 1 10 | url: redis://host.docker.internal:6379 11 | jsonData: 12 | poolSize: 5 13 | timeout: 10 14 | pingInterval: 0 15 | pipelineWindow: 0 16 | editable: true 17 | -------------------------------------------------------------------------------- /provisioning/plugins/redis-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | apps: 4 | - type: redis-app 5 | disabled: false 6 | -------------------------------------------------------------------------------- /src/components/Config/Config.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { setLocationSrv } from '@grafana/runtime'; 4 | import { ApplicationRoot } from '../../constants'; 5 | import { Config } from './Config'; 6 | 7 | /* 8 | Plugin 9 | */ 10 | const getPlugin = (overridePlugin: any = { meta: {} }) => ({ 11 | ...overridePlugin, 12 | meta: { 13 | enabled: true, 14 | ...overridePlugin.meta, 15 | }, 16 | }); 17 | 18 | /* 19 | Config 20 | */ 21 | describe('Config', () => { 22 | beforeAll(() => { 23 | jest.spyOn(Config, 'getLocation').mockImplementation((): any => ({ 24 | assign: jest.fn(), 25 | reload: jest.fn(), 26 | })); 27 | }); 28 | 29 | /* 30 | Initialization 31 | */ 32 | describe('Initialization', () => { 33 | it('If plugin is not enabled and meta is not set, state should have isEnabled = false', () => { 34 | const plugin = getPlugin({}); 35 | const wrapper = shallow(); 36 | expect(wrapper.state().isEnabled).toBeTruthy(); 37 | }); 38 | 39 | it('If plugin is not enabled, state should have isEnabled = false', () => { 40 | const plugin = getPlugin({ meta: { enabled: false } }); 41 | const wrapper = shallow(); 42 | expect(wrapper.state().isEnabled).toBeFalsy(); 43 | }); 44 | 45 | it('If plugin is enabled, state should have isEnabled = true', () => { 46 | const plugin = getPlugin({ meta: { enabled: true } }); 47 | const wrapper = shallow(); 48 | expect(wrapper.state().isEnabled).toBeTruthy(); 49 | }); 50 | }); 51 | 52 | /* 53 | Rendering 54 | */ 55 | describe('rendering', () => { 56 | it('If plugin is not configured, should show enable button', () => { 57 | const plugin = getPlugin({ meta: { enabled: false } }); 58 | const wrapper = shallow(); 59 | const enableButton = wrapper.findWhere((node) => node.name() === 'Button' && node.text() === 'Enable'); 60 | expect(enableButton.exists()).toBeTruthy(); 61 | }); 62 | 63 | it('If plugin is configured, should show disable buttons', () => { 64 | const plugin = getPlugin({ meta: { enabled: true } }); 65 | const wrapper = shallow(); 66 | const disableButton = wrapper.findWhere((node) => node.name() === 'Button' && node.text() === 'Disable'); 67 | expect(disableButton.exists()).toBeTruthy(); 68 | }); 69 | 70 | it('Enable button should call onEnable method', () => { 71 | const plugin = getPlugin({ meta: { enabled: false } }); 72 | const wrapper = shallow(); 73 | const testedMethod = jest.spyOn(wrapper.instance(), 'onEnable').mockImplementation(() => null); 74 | wrapper.instance().forceUpdate(); 75 | const enableButton = wrapper.findWhere((node) => node.name() === 'Button' && node.text() === 'Enable'); 76 | enableButton.simulate('click'); 77 | expect(testedMethod).toHaveBeenCalled(); 78 | }); 79 | 80 | it('Disable button should call onDisable method', () => { 81 | const plugin = getPlugin({ meta: { enabled: true } }); 82 | const wrapper = shallow(); 83 | const testedMethod = jest.spyOn(wrapper.instance(), 'onDisable').mockImplementation(() => null); 84 | wrapper.instance().forceUpdate(); 85 | const disableButton = wrapper.findWhere((node) => node.name() === 'Button' && node.text() === 'Disable'); 86 | disableButton.simulate('click'); 87 | expect(testedMethod).toHaveBeenCalled(); 88 | }); 89 | }); 90 | 91 | /* 92 | Methods 93 | */ 94 | describe('Methods', () => { 95 | it('onDisable should call updatePluginSettings method', () => { 96 | const plugin = getPlugin({ meta: { enabled: true } }); 97 | const wrapper = shallow(); 98 | const testedMethod = jest 99 | .spyOn(wrapper.instance(), 'updatePluginSettings') 100 | .mockImplementation(() => Promise.resolve(null as any)); 101 | wrapper.instance().onDisable(); 102 | expect(testedMethod).toHaveBeenCalledWith({ enabled: false, jsonData: {}, pinned: false }); 103 | }); 104 | 105 | it('onEnable should call updatePluginSettings method', () => { 106 | const plugin = getPlugin({ meta: { enabled: true } }); 107 | const wrapper = shallow(); 108 | const testedMethod = jest 109 | .spyOn(wrapper.instance(), 'updatePluginSettings') 110 | .mockImplementation(() => Promise.resolve(null as any)); 111 | wrapper.instance().onEnable(); 112 | expect(testedMethod).toHaveBeenCalledWith({ enabled: true, jsonData: {}, pinned: true }); 113 | }); 114 | 115 | it('updatePluginSettings should make post request', () => { 116 | const plugin = getPlugin({ meta: { enabled: true, id: 'app' } }); 117 | const wrapper = shallow(); 118 | const postRequestMock = jest.fn(); 119 | wrapper.instance()['backendSrv'] = { 120 | post: postRequestMock, 121 | } as any; 122 | const settings = { enabled: true, jsonData: {}, pinned: true }; 123 | wrapper.instance().updatePluginSettings(settings); 124 | expect(postRequestMock).toHaveBeenCalledWith(`api/plugins/${plugin.meta.id}/settings`, settings); 125 | }); 126 | 127 | it('goHome should redirect on home page', () => { 128 | const updateLocationMock = jest.fn(); 129 | setLocationSrv({ 130 | update: updateLocationMock, 131 | }); 132 | const plugin = getPlugin({ meta: { enabled: true } }); 133 | const wrapper = shallow(); 134 | wrapper.instance().goHome(); 135 | expect(updateLocationMock).toHaveBeenCalledWith({ 136 | path: ApplicationRoot, 137 | partial: false, 138 | }); 139 | }); 140 | }); 141 | 142 | afterAll(() => { 143 | jest.resetAllMocks(); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /src/components/Config/Config.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { AppPluginMeta, PluginConfigPageProps } from '@grafana/data'; 3 | import { BackendSrv, getBackendSrv, getLocationSrv } from '@grafana/runtime'; 4 | import { Button } from '@grafana/ui'; 5 | import { ApplicationName, ApplicationRoot } from '../../constants'; 6 | import { GlobalSettings } from '../../types'; 7 | 8 | /** 9 | * Page Properties 10 | */ 11 | interface Props extends PluginConfigPageProps> {} 12 | 13 | /** 14 | * State 15 | */ 16 | interface State { 17 | isEnabled: boolean; 18 | } 19 | 20 | /** 21 | * Config component 22 | */ 23 | export class Config extends PureComponent { 24 | /** 25 | * Object to get the current page 26 | */ 27 | static getLocation(): Location { 28 | return window.location; 29 | } 30 | 31 | /** 32 | * Service to communicate via http(s) to a remote backend such as the Grafana backend, a datasource etc. 33 | */ 34 | private backendSrv: BackendSrv = getBackendSrv(); 35 | 36 | /** 37 | * Constructor 38 | * 39 | * @param props {Props} Properties 40 | */ 41 | constructor(props: Props) { 42 | super(props); 43 | 44 | this.state = { 45 | isEnabled: false, 46 | }; 47 | } 48 | 49 | /** 50 | * Mount 51 | */ 52 | componentDidMount(): void { 53 | this.setState(() => ({ 54 | isEnabled: this.props.plugin.meta?.enabled ? true : false, 55 | })); 56 | } 57 | 58 | /** 59 | * Home 60 | */ 61 | goHome = (): void => { 62 | getLocationSrv().update({ 63 | path: ApplicationRoot, 64 | partial: false, 65 | }); 66 | }; 67 | 68 | /** 69 | * Plugin Settings 70 | * 71 | * @param settings Plugin Settings 72 | */ 73 | updatePluginSettings = (settings: { enabled: boolean; jsonData: unknown; pinned: boolean }): Promise => { 74 | return this.backendSrv.post(`api/plugins/${this.props.plugin.meta.id}/settings`, settings); 75 | }; 76 | 77 | /** 78 | * Plugin disable 79 | */ 80 | onDisable = () => { 81 | this.updatePluginSettings({ enabled: false, jsonData: {}, pinned: false }).then(() => { 82 | Config.getLocation().reload(); 83 | }); 84 | }; 85 | 86 | /** 87 | * Plugin enable 88 | */ 89 | onEnable = () => { 90 | this.updatePluginSettings({ enabled: true, jsonData: {}, pinned: true }).then(() => { 91 | Config.getLocation().assign(ApplicationRoot); 92 | }); 93 | }; 94 | 95 | /** 96 | * Page Render 97 | */ 98 | render() { 99 | const { isEnabled } = this.state; 100 | 101 | return ( 102 | <> 103 |

{ApplicationName}

104 |

The Redis Application, is a plugin for Grafana that provides custom panels for Redis Data Source.

105 | {!isEnabled && ( 106 |

107 | Click below to Enable the Application and start monitoring your Redis instances today. 108 |

109 | )} 110 |
111 | {isEnabled ? ( 112 | 115 | ) : ( 116 | 117 | )} 118 |
119 | 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/components/Config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Config'; 2 | -------------------------------------------------------------------------------- /src/components/DataSourceList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DataSourceList'; 2 | -------------------------------------------------------------------------------- /src/components/RootPage/RootPage.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { Observable } from 'rxjs'; 4 | import { AppPluginMeta, PluginType } from '@grafana/data'; 5 | import { Alert } from '@grafana/ui'; 6 | import { ApplicationName, ApplicationSubTitle, DataSourceType, RedisCommand } from '../../constants'; 7 | import { DataSourceList } from '../DataSourceList'; 8 | import { RootPage } from './RootPage'; 9 | 10 | /** 11 | * Meta 12 | */ 13 | const getMeta = (): AppPluginMeta => ({ 14 | id: '', 15 | name: '', 16 | type: PluginType.app, 17 | module: '', 18 | baseUrl: '', 19 | info: { 20 | author: {} as any, 21 | description: '', 22 | logos: { 23 | large: '', 24 | small: '', 25 | }, 26 | links: [], 27 | screenshots: [], 28 | updated: '', 29 | version: '', 30 | }, 31 | }); 32 | 33 | /** 34 | * DataSourceMock 35 | */ 36 | const getDataSourceMock = jest.fn().mockImplementation(() => Promise.resolve([])); 37 | 38 | /** 39 | * RedisMock 40 | */ 41 | const redisMock = { 42 | query: jest.fn().mockImplementation( 43 | () => 44 | new Observable((subscriber) => { 45 | subscriber.next({ 46 | data: [ 47 | { 48 | fields: [ 49 | { 50 | values: { 51 | toArray() { 52 | return ['info', '2', '3']; 53 | }, 54 | }, 55 | }, 56 | ], 57 | length: 1, 58 | }, 59 | ], 60 | }); 61 | subscriber.complete(); 62 | }) 63 | ), 64 | }; 65 | /** 66 | * GetRedisMock 67 | */ 68 | const getRedisMock = jest.fn().mockImplementation(() => Promise.resolve(redisMock)); 69 | 70 | /** 71 | * Mock @grafana/runtime 72 | */ 73 | jest.mock('@grafana/runtime', () => ({ 74 | getBackendSrv: () => ({ 75 | get: getDataSourceMock, 76 | }), 77 | getDataSourceSrv: () => ({ 78 | get: getRedisMock, 79 | }), 80 | })); 81 | 82 | /** 83 | * RootPage 84 | */ 85 | describe('RootPage', () => { 86 | const meta = getMeta(); 87 | const path = '/app'; 88 | const onNavChangedMock = jest.fn(); 89 | 90 | beforeAll(() => { 91 | Object.defineProperty(window, 'location', { 92 | value: { reload: jest.fn() }, 93 | }); 94 | }); 95 | 96 | beforeEach(() => { 97 | onNavChangedMock.mockClear(); 98 | getDataSourceMock.mockClear(); 99 | getRedisMock.mockClear(); 100 | redisMock.query.mockClear(); 101 | }); 102 | 103 | /** 104 | * Mounting 105 | */ 106 | describe('Mounting', () => { 107 | it('Should update navigation', () => { 108 | const wrapper = shallow( 109 | 110 | ); 111 | const testedMethod = jest.spyOn(wrapper.instance(), 'updateNav'); 112 | wrapper.instance().componentDidMount(); 113 | expect(testedMethod).toHaveBeenCalledTimes(1); 114 | }); 115 | 116 | it('Should make get /api/datasources request', () => { 117 | const wrapper = shallow( 118 | 119 | ); 120 | wrapper.instance().componentDidMount(); 121 | expect(getDataSourceMock).toHaveBeenCalledWith('/api/datasources'); 122 | }); 123 | 124 | it('Should check supported commands', (done) => { 125 | getDataSourceMock.mockImplementationOnce(() => 126 | Promise.resolve([ 127 | { 128 | type: DataSourceType.REDIS, 129 | name: 'redis', 130 | }, 131 | ]) 132 | ); 133 | const wrapper = shallow( 134 | 135 | ); 136 | wrapper.instance().componentDidMount(); 137 | 138 | setImmediate(() => { 139 | expect(getRedisMock).toHaveBeenCalledWith('redis'); 140 | expect(redisMock.query).toHaveBeenCalledWith({ targets: [{ refId: 'A', query: RedisCommand.COMMAND }] }); 141 | expect(wrapper.state().loading).toBeFalsy(); 142 | expect(wrapper.state().dataSources).toEqual([ 143 | { 144 | type: DataSourceType.REDIS, 145 | name: 'redis', 146 | commands: ['INFO'], 147 | }, 148 | ]); 149 | done(); 150 | }); 151 | }); 152 | }); 153 | 154 | /** 155 | * updateNav 156 | */ 157 | describe('updateNav', () => { 158 | it('Should call onNavChanged prop', () => { 159 | const wrapper = shallow( 160 | 161 | ); 162 | wrapper.instance().updateNav(); 163 | const node = { 164 | text: ApplicationName, 165 | img: meta.info.logos.large, 166 | subTitle: ApplicationSubTitle, 167 | url: path, 168 | children: [ 169 | { 170 | text: 'Home', 171 | url: path, 172 | id: 'home', 173 | icon: 'fa fa-fw fa-home', 174 | active: true, 175 | }, 176 | ], 177 | }; 178 | expect(onNavChangedMock).toHaveBeenCalledWith({ 179 | node: node, 180 | main: node, 181 | }); 182 | }); 183 | }); 184 | 185 | /** 186 | * Rendering 187 | */ 188 | describe('rendering', () => { 189 | it('Should show message if loading=true', (done) => { 190 | const wrapper = shallow( 191 | 192 | ); 193 | const loadingMessageComponent = wrapper.findWhere( 194 | (node) => node.is(Alert) && node.prop('title') === 'Loading...' 195 | ); 196 | expect(loadingMessageComponent.exists()).toBeTruthy(); 197 | wrapper.instance().componentDidMount(); 198 | setImmediate(() => { 199 | const dataSourceListComponent = wrapper.findWhere((node) => node.is(DataSourceList)); 200 | const loadingMessageComponent = wrapper.findWhere( 201 | (node) => node.is(Alert) && node.prop('title') === 'Loading...' 202 | ); 203 | expect(loadingMessageComponent.exists()).not.toBeTruthy(); 204 | expect(dataSourceListComponent.exists()).toBeTruthy(); 205 | expect(dataSourceListComponent.prop('dataSources')).toEqual(wrapper.state().dataSources); 206 | done(); 207 | }); 208 | }); 209 | 210 | it('If dataSource is unable to make query, should work correctly', async () => { 211 | const wrapper = shallow( 212 | , 213 | { disableLifecycleMethods: true } 214 | ); 215 | 216 | await wrapper.instance().componentDidMount(); 217 | 218 | const dataSourceListComponent = wrapper.findWhere((node) => node.is(DataSourceList)); 219 | const loadingMessageComponent = wrapper.findWhere( 220 | (node) => node.is(Alert) && node.prop('title') === 'Loading...' 221 | ); 222 | expect(loadingMessageComponent.exists()).not.toBeTruthy(); 223 | expect(dataSourceListComponent.exists()).toBeTruthy(); 224 | }); 225 | }); 226 | 227 | afterAll(() => { 228 | jest.resetAllMocks(); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /src/components/RootPage/RootPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { lastValueFrom, Observable } from 'rxjs'; 3 | import { 4 | AppRootProps, 5 | DataQueryRequest, 6 | DataQueryResponse, 7 | DataQueryResponseData, 8 | Field, 9 | NavModelItem, 10 | } from '@grafana/data'; 11 | import { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; 12 | import { Alert } from '@grafana/ui'; 13 | import { ApplicationName, ApplicationSubTitle, DataSourceType, RedisCommand } from '../../constants'; 14 | import { GlobalSettings, RedisDataSourceInstanceSettings, RedisQuery } from '../../types'; 15 | import { DataSourceList } from '../DataSourceList'; 16 | 17 | /** 18 | * Properties 19 | */ 20 | interface Props extends AppRootProps {} 21 | 22 | /** 23 | * State 24 | */ 25 | interface State { 26 | /** 27 | * Data sources 28 | * 29 | * @type {RedisDataSourceInstanceSettings[]} 30 | */ 31 | dataSources: RedisDataSourceInstanceSettings[]; 32 | 33 | /** 34 | * Loading 35 | * 36 | * @type {boolean} 37 | */ 38 | loading: boolean; 39 | } 40 | 41 | /** 42 | * Root Page 43 | */ 44 | export class RootPage extends PureComponent { 45 | /** 46 | * Default state 47 | */ 48 | state: State = { 49 | loading: true, 50 | dataSources: [], 51 | }; 52 | 53 | /** 54 | * Mount 55 | */ 56 | async componentDidMount() { 57 | this.updateNav(); 58 | 59 | /** 60 | * Get data sources 61 | */ 62 | const dataSources = await getBackendSrv() 63 | .get('/api/datasources') 64 | .then((result: RedisDataSourceInstanceSettings[]) => { 65 | return result.filter((ds: RedisDataSourceInstanceSettings) => { 66 | return ds.type === DataSourceType.REDIS; 67 | }); 68 | }); 69 | 70 | /** 71 | * Workaround, until reload function will be added to DataSourceSrv 72 | * 73 | * @see https://github.com/grafana/grafana/issues/30728 74 | * @see https://github.com/grafana/grafana/issues/29809 75 | */ 76 | await getBackendSrv() 77 | .get('/api/frontend/settings') 78 | .then((settings: any) => { 79 | if (!settings.datasources) { 80 | return; 81 | } 82 | 83 | /** 84 | * Set data sources 85 | */ 86 | config.datasources = settings.datasources; 87 | config.defaultDatasource = settings.defaultDatasource; 88 | }); 89 | 90 | /** 91 | * Check supported commands for Redis Data Sources 92 | */ 93 | await Promise.all( 94 | dataSources.map(async (ds: RedisDataSourceInstanceSettings) => { 95 | ds.commands = []; 96 | 97 | /** 98 | * Get Data Source 99 | */ 100 | const redis = await getDataSourceSrv().get(ds.name); 101 | 102 | /** 103 | * Execute query 104 | */ 105 | const dsQuery = redis.query({ 106 | targets: [{ refId: 'A', query: RedisCommand.COMMAND }], 107 | } as DataQueryRequest) as unknown; 108 | 109 | const query = lastValueFrom(dsQuery as Observable); 110 | if (!query) { 111 | return; 112 | } 113 | 114 | /** 115 | * Get available commands 116 | */ 117 | await query 118 | .then((response: DataQueryResponse) => response.data) 119 | .then((data: DataQueryResponseData[]) => 120 | data.forEach((item: DataQueryResponseData) => { 121 | item.fields.forEach((field: Field) => { 122 | ds.commands.push( 123 | ...field.values 124 | .toArray() 125 | .filter((value: string) => value.match(/\S+\.\S+|INFO/i)) 126 | .map((value) => value.toUpperCase()) 127 | ); 128 | }); 129 | }) 130 | ) 131 | .catch(() => {}); 132 | }) 133 | ); 134 | 135 | /** 136 | * Set state 137 | */ 138 | this.setState({ 139 | dataSources, 140 | loading: false, 141 | }); 142 | } 143 | 144 | /** 145 | * Navigation 146 | */ 147 | updateNav() { 148 | const { path, onNavChanged, meta } = this.props; 149 | const tabs: NavModelItem[] = []; 150 | 151 | /** 152 | * Home 153 | */ 154 | tabs.push({ 155 | text: 'Home', 156 | url: path, 157 | id: 'home', 158 | icon: 'fa fa-fw fa-home', 159 | active: true, 160 | }); 161 | 162 | /** 163 | * Header 164 | */ 165 | const node = { 166 | text: ApplicationName, 167 | img: meta.info.logos.large, 168 | subTitle: ApplicationSubTitle, 169 | url: path, 170 | children: tabs, 171 | }; 172 | 173 | /** 174 | * Update the page header 175 | */ 176 | onNavChanged({ 177 | node: node, 178 | main: node, 179 | }); 180 | } 181 | 182 | /** 183 | * Render 184 | */ 185 | render() { 186 | const { loading, dataSources } = this.state; 187 | 188 | /** 189 | * Loading 190 | */ 191 | if (loading) { 192 | return ( 193 | 194 |

Loading time depends on the number of configured data sources.

195 |
196 | ); 197 | } 198 | 199 | return ; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/components/RootPage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RootPage'; 2 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Config'; 2 | export * from './DataSourceList'; 3 | export * from './RootPage'; 4 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Data Source types 3 | */ 4 | export enum DataSourceType { 5 | REDIS = 'redis-datasource', 6 | } 7 | 8 | /** 9 | * New Data Source names 10 | */ 11 | export enum DataSourceName { 12 | REDIS = 'Redis', 13 | } 14 | 15 | /** 16 | * Redis commands 17 | */ 18 | export enum RedisCommand { 19 | COMMAND = 'command', 20 | REDISGEARS = 'RG.PYEXECUTE', 21 | REDISTIMESERIES = 'TS.INFO', 22 | REDISAI = 'AI.INFO', 23 | REDISEARCH = 'FT.INFO', 24 | REDISJSON = 'JSON.GET', 25 | REDISGRAPH = 'GRAPH.QUERY', 26 | REDISBLOOM = 'BF.INFO', 27 | } 28 | 29 | /** 30 | * Client Type Values 31 | */ 32 | export enum ClientTypeValue { 33 | CLUSTER = 'cluster', 34 | SENTINEL = 'sentinel', 35 | SOCKET = 'socket', 36 | STANDALONE = 'standalone', 37 | } 38 | 39 | /** 40 | * Application root page 41 | */ 42 | export const ApplicationRoot = '/a/redis-app'; 43 | 44 | /** 45 | * Application 46 | */ 47 | export const ApplicationName = 'Redis Application'; 48 | export const ApplicationSubTitle = 'Redis Data Source Manager'; 49 | -------------------------------------------------------------------------------- /src/icons/HighAvailability.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * High Availability 6 | */ 7 | export const HighAvailability: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 10 | 11 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/icons/MultiLayerSecurity.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * Multi Layer Security 6 | */ 7 | export const MultiLayerSecurity: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 10 | 20 | 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/icons/RediSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * RediSearch 6 | */ 7 | export const RediSearch: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 15 | 16 | 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/icons/RedisAI.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * RedisAI 6 | */ 7 | export const RedisAI: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 15 | 16 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/icons/RedisBloom.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * RedisBloom 6 | */ 7 | export const RedisBloom: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 15 | 16 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/icons/RedisCube.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * Redis cube 6 | */ 7 | export const RedisCube: FC = ({ size, fill, title, ...rest }) => { 8 | return ( 9 | 15 | 16 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/icons/RedisGears.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * RedisGears 6 | */ 7 | export const RedisGears: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 15 | 16 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/icons/RedisGraph.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * RedisGraph 6 | */ 7 | export const RedisGraph: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 15 | 16 | 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/icons/RedisJSON.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * RedisJSON 6 | */ 7 | export const RedisJSON: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 15 | 16 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/icons/RedisTimeSeries.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { SVGProps } from '../types'; 3 | 4 | /** 5 | * RedisTimeSeries 6 | */ 7 | export const RedisTimeSeries: FC = ({ size, fill, ...rest }) => { 8 | return ( 9 | 15 | 16 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HighAvailability'; 2 | export * from './MultiLayerSecurity'; 3 | export * from './RedisAI'; 4 | export * from './RedisBloom'; 5 | export * from './RedisCube'; 6 | export * from './RediSearch'; 7 | export * from './RedisGears'; 8 | export * from './RedisGraph'; 9 | export * from './RedisJSON'; 10 | export * from './RedisTimeSeries'; 11 | -------------------------------------------------------------------------------- /src/img/enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/enable.png -------------------------------------------------------------------------------- /src/img/grafana-marketplace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/grafana-marketplace.png -------------------------------------------------------------------------------- /src/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | redis 32 8 | 21 | -------------------------------------------------------------------------------- /src/img/redis-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-app.png -------------------------------------------------------------------------------- /src/img/redis-cli-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-cli-dashboard.png -------------------------------------------------------------------------------- /src/img/redis-cli-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-cli-panel.png -------------------------------------------------------------------------------- /src/img/redis-cpu-usage-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-cpu-usage-graph.png -------------------------------------------------------------------------------- /src/img/redis-gears-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-gears-dashboard.png -------------------------------------------------------------------------------- /src/img/redis-keys-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-keys-panel.png -------------------------------------------------------------------------------- /src/img/redis-latency-panel-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-latency-panel-graph.png -------------------------------------------------------------------------------- /src/img/redis-latency-panel-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedisGrafana/grafana-redis-app/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/img/redis-latency-panel-table.png -------------------------------------------------------------------------------- /src/module.test.ts: -------------------------------------------------------------------------------- 1 | import { AppPlugin } from '@grafana/data'; 2 | import { plugin } from './module'; 3 | 4 | /* 5 | Plugin 6 | */ 7 | describe('plugin', () => { 8 | it('Should be instance of AppPlugin', () => { 9 | expect(plugin).toBeInstanceOf(AppPlugin); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { AppPlugin } from '@grafana/data'; 2 | import { Config, RootPage } from './components'; 3 | import { GlobalSettings } from './types'; 4 | 5 | /** 6 | * Application Plugin 7 | */ 8 | export const plugin = new AppPlugin().setRootPage(RootPage).addConfigPage({ 9 | title: 'Config', 10 | body: Config, 11 | id: 'config', 12 | }); 13 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "grafanaDependency": ">=8.0.0", 4 | "grafanaVersion": "8.x.x", 5 | "plugins": [ 6 | { 7 | "id": "redis-datasource", 8 | "name": "Redis Data Source", 9 | "type": "datasource", 10 | "version": "^2.1.1" 11 | } 12 | ] 13 | }, 14 | "id": "redis-app", 15 | "includes": [ 16 | { 17 | "addToNav": true, 18 | "defaultNav": true, 19 | "icon": "home-alt", 20 | "name": "Home", 21 | "path": "/a/redis-app/", 22 | "role": "Admin", 23 | "type": "page" 24 | }, 25 | { 26 | "addToNav": false, 27 | "name": "Redis Overview", 28 | "path": "dashboards/redis-overview.json", 29 | "role": "Admin", 30 | "type": "dashboard" 31 | }, 32 | { 33 | "addToNav": false, 34 | "name": "Redis CLI", 35 | "path": "dashboards/redis-cli.json", 36 | "role": "Admin", 37 | "type": "dashboard" 38 | }, 39 | { 40 | "addToNav": false, 41 | "name": "RedisGears", 42 | "path": "dashboards/redis-gears.json", 43 | "role": "Admin", 44 | "type": "dashboard" 45 | }, 46 | { 47 | "addToNav": true, 48 | "icon": "monitor", 49 | "name": "Redis Overview", 50 | "path": "/d/RpSjVqWMz", 51 | "role": "Admin", 52 | "type": "page" 53 | }, 54 | { 55 | "addToNav": true, 56 | "icon": "calculator-alt", 57 | "name": "Redis CLI", 58 | "path": "/d/_SGxCBNGk", 59 | "role": "Admin", 60 | "type": "page" 61 | }, 62 | { 63 | "addToNav": true, 64 | "icon": "cog", 65 | "name": "RedisGears", 66 | "path": "/d/xFPiNzLMz", 67 | "role": "Admin", 68 | "type": "page" 69 | }, 70 | { 71 | "name": "Redis CLI Panel", 72 | "type": "panel" 73 | }, 74 | { 75 | "name": "Redis Latency Panel", 76 | "type": "panel" 77 | }, 78 | { 79 | "name": "RedisGears Panel", 80 | "type": "panel" 81 | }, 82 | { 83 | "name": "Redis CPU Panel", 84 | "type": "panel" 85 | } 86 | ], 87 | "info": { 88 | "author": { 89 | "name": "RedisGrafana", 90 | "url": "https://redisgrafana.github.io" 91 | }, 92 | "description": "Provides Application pages and custom panels for Redis Data Source.", 93 | "keywords": ["redis", "timeseries", "plugin"], 94 | "links": [ 95 | { 96 | "name": "Website", 97 | "url": "https://redisgrafana.github.io" 98 | }, 99 | { 100 | "name": "License", 101 | "url": "https://github.com/RedisGrafana/grafana-redis-app/blob/master/LICENSE" 102 | } 103 | ], 104 | "logos": { 105 | "large": "img/logo.svg", 106 | "small": "img/logo.svg" 107 | }, 108 | "screenshots": [ 109 | { 110 | "name": "Redis Application", 111 | "path": "img/redis-app.png" 112 | }, 113 | { 114 | "name": "Redis CLI Dashboard", 115 | "path": "img/redis-cli-dashboard.png" 116 | }, 117 | { 118 | "name": "RedisGears Dashboard", 119 | "path": "img/redis-gears-dashboard.png" 120 | }, 121 | { 122 | "name": "Redis CLI Panel", 123 | "path": "img/redis-cli-panel.png" 124 | }, 125 | { 126 | "name": "Redis Latency Panel", 127 | "path": "img/redis-latency-panel-graph.png" 128 | }, 129 | { 130 | "name": "Max Memory Keys Panel", 131 | "path": "img/redis-keys-panel.png" 132 | }, 133 | { 134 | "name": "Redis CPU Panel", 135 | "path": "img/redis-cpu-usage-graph.png" 136 | } 137 | ], 138 | "updated": "%TODAY%", 139 | "version": "%VERSION%" 140 | }, 141 | "name": "Redis Application", 142 | "type": "app" 143 | } 144 | -------------------------------------------------------------------------------- /src/redis-cli-panel/components/AutoScrollingTextArea/AutoScrollingTextArea.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { TextArea } from '@grafana/ui'; 4 | import { CLITextArea } from './AutoScrollingTextArea'; 5 | 6 | /** 7 | * CLI TextArea 8 | */ 9 | describe('CLITextArea', () => { 10 | it('Should set scrollTop if autoScroll=true', () => { 11 | const wrapper = shallow(); 12 | jest.spyOn(wrapper.instance(), 'render'); 13 | const element = { 14 | scrollHeight: 0, 15 | scrollTop: 0, 16 | }; 17 | 18 | expect(element.scrollTop).toEqual(element.scrollHeight); 19 | }); 20 | 21 | it('Should pass props to Textarea', () => { 22 | const onChangeMock = jest.fn(); 23 | const value = '1234'; 24 | const wrapper = shallow(); 25 | const testedComponent = wrapper.find(TextArea); 26 | expect(testedComponent.exists()).toBeTruthy(); 27 | expect(testedComponent.prop('value')).toEqual(value); 28 | expect(testedComponent.prop('onChange')).toEqual(onChangeMock); 29 | }); 30 | 31 | /* 32 | * Waiting for React 17 support in Enzyme 33 | * it('Should set ref', () => { 34 | * const wrapper = mount(); 35 | * expect(wrapper.instance()).toBeTruthy(); 36 | * }); 37 | */ 38 | }); 39 | -------------------------------------------------------------------------------- /src/redis-cli-panel/components/AutoScrollingTextArea/AutoScrollingTextArea.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextArea } from '@grafana/ui'; 3 | 4 | /** 5 | * Properties 6 | */ 7 | type TextareaProps = React.TextareaHTMLAttributes; 8 | 9 | /** 10 | * Auto scrolling text area 11 | */ 12 | export class AutoScrollingTextArea extends React.Component { 13 | element: HTMLTextAreaElement | null | undefined; 14 | 15 | /** 16 | * Mount 17 | */ 18 | componentDidMount() { 19 | if (this.element && this.props.autoScroll !== false) { 20 | this.element.scrollTop = this.element.scrollHeight; 21 | } 22 | } 23 | 24 | /** 25 | * Update 26 | * 27 | * @param prevProps {TextareaProps} 28 | * @param prevState 29 | */ 30 | componentDidUpdate(prevProps: TextareaProps, prevState: {}) { 31 | if (prevProps.value !== this.props.value && this.element && this.props.autoScroll !== false) { 32 | this.element.scrollTop = this.element.scrollHeight; 33 | } 34 | } 35 | 36 | /** 37 | * Render 38 | */ 39 | render() { 40 | return