Browse Source

feat: Initialize fullstack-test application with essential configurations and components

- Add package.json with scripts and dependencies for development, testing, and building.
- Create Playwright configuration for end-to-end testing.
- Add provisioning files for Grafana plugin setup.
- Implement core application structure with routing and lazy-loaded components.
- Develop pages (PageOne, PageTwo, PageThree, PageFour) with respective functionalities.
- Create AppConfig component for plugin configuration with API settings.
- Implement tests for App and AppConfig components.
- Add utility functions for routing and test fixtures.
- Include TypeScript configuration for the project.
- Add logo and necessary assets for the application.
master
Matteo Benedetto 3 months ago
commit
e0c7337ff5
  1. 36
      .gitignore
  2. 213
      README.md
  3. 3
      enne2corp-fullstacktest-app/.config/.cprc.json
  4. 30
      enne2corp-fullstacktest-app/.config/.eslintrc
  5. 16
      enne2corp-fullstacktest-app/.config/.prettierrc.js
  6. 77
      enne2corp-fullstacktest-app/.config/Dockerfile
  7. 165
      enne2corp-fullstacktest-app/.config/README.md
  8. 43
      enne2corp-fullstacktest-app/.config/bundler/externals.ts
  9. 31
      enne2corp-fullstacktest-app/.config/docker-compose-base.yaml
  10. 18
      enne2corp-fullstacktest-app/.config/entrypoint.sh
  11. 28
      enne2corp-fullstacktest-app/.config/jest-setup.js
  12. 44
      enne2corp-fullstacktest-app/.config/jest.config.js
  13. 25
      enne2corp-fullstacktest-app/.config/jest/mocks/react-inlinesvg.tsx
  14. 37
      enne2corp-fullstacktest-app/.config/jest/utils.js
  15. 47
      enne2corp-fullstacktest-app/.config/supervisord/supervisord.conf
  16. 26
      enne2corp-fullstacktest-app/.config/tsconfig.json
  17. 37
      enne2corp-fullstacktest-app/.config/types/bundler-rules.d.ts
  18. 83
      enne2corp-fullstacktest-app/.config/types/webpack-plugins.d.ts
  19. 33
      enne2corp-fullstacktest-app/.config/webpack/BuildModeWebpackPlugin.ts
  20. 2
      enne2corp-fullstacktest-app/.config/webpack/constants.ts
  21. 68
      enne2corp-fullstacktest-app/.config/webpack/utils.ts
  22. 247
      enne2corp-fullstacktest-app/.config/webpack/webpack.config.ts
  23. 7
      enne2corp-fullstacktest-app/.cprc.json
  24. 3
      enne2corp-fullstacktest-app/.eslintrc
  25. 27
      enne2corp-fullstacktest-app/.github/workflows/bundle-stats.yml
  26. 241
      enne2corp-fullstacktest-app/.github/workflows/ci.yml
  27. 26
      enne2corp-fullstacktest-app/.github/workflows/cp-update.yml
  28. 37
      enne2corp-fullstacktest-app/.github/workflows/is-compatible.yml
  29. 29
      enne2corp-fullstacktest-app/.github/workflows/release.yml
  30. 46
      enne2corp-fullstacktest-app/.gitignore
  31. 1
      enne2corp-fullstacktest-app/.nvmrc
  32. 4
      enne2corp-fullstacktest-app/.prettierrc.js
  33. 5
      enne2corp-fullstacktest-app/CHANGELOG.md
  34. 201
      enne2corp-fullstacktest-app/LICENSE
  35. 12
      enne2corp-fullstacktest-app/Magefile.go
  36. 136
      enne2corp-fullstacktest-app/README.md
  37. 5
      enne2corp-fullstacktest-app/docker-compose.yaml
  38. 66
      enne2corp-fullstacktest-app/go.mod
  39. 405
      enne2corp-fullstacktest-app/go.sum
  40. 2
      enne2corp-fullstacktest-app/jest-setup.js
  41. 8
      enne2corp-fullstacktest-app/jest.config.js
  42. 14979
      enne2corp-fullstacktest-app/package-lock.json
  43. 80
      enne2corp-fullstacktest-app/package.json
  44. 53
      enne2corp-fullstacktest-app/playwright.config.ts
  45. 4
      enne2corp-fullstacktest-app/provisioning/plugins/README.md
  46. 11
      enne2corp-fullstacktest-app/provisioning/plugins/apps.yaml
  47. 50
      enne2corp-fullstacktest-app/src/README.md
  48. 38
      enne2corp-fullstacktest-app/src/components/App/App.test.tsx
  49. 25
      enne2corp-fullstacktest-app/src/components/App/App.tsx
  50. 38
      enne2corp-fullstacktest-app/src/components/AppConfig/AppConfig.test.tsx
  51. 140
      enne2corp-fullstacktest-app/src/components/AppConfig/AppConfig.tsx
  52. 21
      enne2corp-fullstacktest-app/src/components/testIds.ts
  53. 10
      enne2corp-fullstacktest-app/src/constants.ts
  54. 1
      enne2corp-fullstacktest-app/src/img/logo.svg
  55. 26
      enne2corp-fullstacktest-app/src/module.tsx
  56. 44
      enne2corp-fullstacktest-app/src/pages/PageFour.tsx
  57. 165
      enne2corp-fullstacktest-app/src/pages/PageOne.tsx
  58. 49
      enne2corp-fullstacktest-app/src/pages/PageThree.tsx
  59. 15
      enne2corp-fullstacktest-app/src/pages/PageTwo.tsx
  60. 64
      enne2corp-fullstacktest-app/src/plugin.json
  61. 6
      enne2corp-fullstacktest-app/src/utils/utils.routing.ts
  62. 19
      enne2corp-fullstacktest-app/tests/appConfig.spec.ts
  63. 31
      enne2corp-fullstacktest-app/tests/appNavigation.spec.ts
  64. 26
      enne2corp-fullstacktest-app/tests/fixtures.ts
  65. 3
      enne2corp-fullstacktest-app/tsconfig.json

36
.gitignore vendored

@ -0,0 +1,36 @@
# Node.js dependencies
node_modules/
dist/
build/
.env
.env.local
# Go build artifacts
*.exe
*.dll
*.so
*.dylib
*.test
*.out
*.log
# Mage build output
mage_output/
bin/
pkg/
# OS files
.DS_Store
Thumbs.db
# IDE/editor files
.vscode/
.idea/
*.swp
*.swo
# Grafana plugin dev files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
coverage/

213
README.md

@ -0,0 +1,213 @@
# Guida: Come Creare un Plugin Grafana con Frontend e Backend
## Introduzione
Questa guida spiega come creare un plugin Grafana completo che include sia frontend (React/TypeScript) che backend (Go), con funzionalità per salvare e recuperare testi tramite API.
## Cosa abbiamo creato
Un'applicazione Grafana che permette di:
- Inserire un messaggio di testo tramite un form
- Salvare il messaggio nel backend (in un file)
- Caricare e visualizzare il messaggio salvato come messaggio di benvenuto
- Gestire stati di caricamento ed errori
## Passaggi Realizzati
### 1. Creazione del Plugin Base
```bash
# Navigazione nella directory di lavoro
cd /home/enne2/Sviluppo/grafana-plugin
# Creazione del plugin usando lo strumento ufficiale
npx @grafana/create-plugin@latest
```
**Configurazione selezionata:**
- Tipo di plugin: **app** (applicazione)
- Backend: **Sì** (per supportare funzionalità server-side)
- Nome plugin: **fullstack-test**
- Organizzazione: **enne2 corp**
Il comando ha generato automaticamente:
- Struttura completa del progetto
- File di configurazione (package.json, tsconfig.json, docker-compose.yaml)
- Template per frontend e backend
- Test end-to-end con Playwright
- GitHub Actions per CI/CD
### 2. Installazione delle Dipendenze
```bash
cd enne2corp-fullstacktest-app
npm install
```
### 3. Implementazione del Frontend
#### Modifica di PageOne.tsx
Ho modificato il file `src/pages/PageOne.tsx` per aggiungere:
**Import necessari:**
```typescript
import React, { useState, useEffect } from 'react';
import { Input, Button, Alert } from '@grafana/ui';
import { getBackendSrv } from '@grafana/runtime';
```
**Stato del componente:**
```typescript
const [textValue, setTextValue] = useState('');
const [savedValue, setSavedValue] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
```
**Funzioni principali:**
- `loadSavedValue()`: Carica il messaggio salvato dal backend
- `handleSaveText()`: Invia il nuovo messaggio al backend
**UI implementata:**
- Messaggio di benvenuto (se presente un valore salvato)
- Form con input di testo e pulsante
- Gestione di stati di caricamento, errori e successo
- Styling personalizzato usando Emotion CSS
### 4. Implementazione del Backend
#### Modifica di resources.go
Ho aggiunto due nuovi endpoint nel file `pkg/plugin/resources.go`:
**1. Endpoint per salvare testo (`/save-text`):**
```go
func (a *App) handleSaveText(w http.ResponseWriter, req *http.Request) {
// Validazione metodo POST
// Decodifica JSON del messaggio
// Creazione directory di dati se necessaria
// Salvataggio in file /tmp/grafana-plugin-data/saved_message.txt
// Risposta JSON di conferma
}
```
**2. Endpoint per recuperare testo (`/get-text`):**
```go
func (a *App) handleGetText(w http.ResponseWriter, req *http.Request) {
// Validazione metodo GET
// Controllo esistenza file
// Lettura contenuto file
// Risposta JSON con il messaggio
}
```
**Registrazione dei nuovi endpoint:**
```go
func (a *App) registerRoutes(mux *http.ServeMux) {
mux.HandleFunc("/ping", a.handlePing)
mux.HandleFunc("/echo", a.handleEcho)
mux.HandleFunc("/save-text", a.handleSaveText) // NUOVO
mux.HandleFunc("/get-text", a.handleGetText) // NUOVO
}
```
#### Correzione di app.go
Ho dovuto correggere la funzione `NewApp` per rimuovere il parametro `context.Context` che causava un errore di compilazione:
```go
// PRIMA (errato)
func NewApp(ctx context.Context, settings backend.AppInstanceSettings) (instancemgmt.Instance, error)
// DOPO (corretto)
func NewApp(settings backend.AppInstanceSettings) (instancemgmt.Instance, error)
```
### 5. Installazione di Go e Mage
```bash
# Installazione di Go
sudo apt update && sudo apt install -y golang-go
# Installazione di Mage (build tool)
go install github.com/magefile/mage@latest
# Aggiunta al PATH
export PATH=$PATH:$(go env GOPATH)/bin
```
### 6. Build del Backend
```bash
mage -v build:linux
```
Questo comando ha compilato il codice Go e creato l'eseguibile `dist/gpx_fullstack_test_linux_amd64`.
## Struttura File Principali
```
enne2corp-fullstacktest-app/
├── src/
│ ├── pages/
│ │ └── PageOne.tsx # Frontend con form e logica
│ └── components/
├── pkg/
│ ├── main.go # Entry point del backend
│ └── plugin/
│ ├── app.go # Configurazione app
│ └── resources.go # API endpoints
├── package.json # Dipendenze frontend
├── go.mod # Dipendenze backend
├── docker-compose.yaml # Ambiente di sviluppo
└── Magefile.go # Build configuration
```
## Funzionalità Implementate
### Frontend (React/TypeScript)
- **Form di input**: Campo testo + pulsante per inserire messaggi
- **Caricamento automatico**: All'avvio carica il messaggio salvato
- **Messaggio di benvenuto**: Mostra il testo salvato in evidenza
- **Gestione stati**: Loading, errori, successo con feedback visuale
- **Styling**: Design coerente con il tema Grafana
### Backend (Go)
- **API REST**: Due endpoint `/save-text` e `/get-text`
- **Persistenza**: Salvataggio in file system (`/tmp/grafana-plugin-data/`)
- **Validazione**: Controllo input e gestione errori
- **JSON**: Comunicazione via JSON tra frontend e backend
### Comunicazione Frontend-Backend
- **Salvataggio**: `POST /api/plugins/enne2corp-fullstacktest-app/resources/save-text`
- **Recupero**: `GET /api/plugins/enne2corp-fullstacktest-app/resources/get-text`
- **Grafana Backend Service**: Uso di `getBackendSrv()` per le chiamate API
## Prossimi Passi per Completare
Per testare l'applicazione completa:
1. **Build del frontend:**
```bash
npm run dev
```
2. **Avvio ambiente di sviluppo:**
```bash
npm run server
```
3. **Apertura di Grafana:**
- URL: http://localhost:3000
- Navigare al plugin per testare le funzionalità
## Note Tecniche
- **Persistenza**: I dati sono salvati in `/tmp/grafana-plugin-data/saved_message.txt`
- **CORS**: Gestito automaticamente dal framework Grafana
- **Autenticazione**: Gestita dal sistema Grafana
- **Errori**: Logging sia frontend che backend per debugging
Questa implementazione fornisce una base solida per plugin Grafana più complessi che richiedono comunicazione bidirezionale tra frontend e backend.

3
enne2corp-fullstacktest-app/.config/.cprc.json

@ -0,0 +1,3 @@
{
"version": "5.26.9"
}

30
enne2corp-fullstacktest-app/.config/.eslintrc

@ -0,0 +1,30 @@
/*
* ⚠ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-eslint-config
*/
{
"extends": ["@grafana/eslint-config"],
"root": true,
"rules": {
"react/prop-types": "off"
},
"overrides": [
{
"files": ["src/**/*.{ts,tsx}"],
"rules": {
"@typescript-eslint/no-deprecated": "warn"
},
"parserOptions": {
"project": "./tsconfig.json"
}
},
{
"files": ["./tests/**/*"],
"rules": {
"react-hooks/rules-of-hooks": "off"
}
}
]
}

16
enne2corp-fullstacktest-app/.config/.prettierrc.js

@ -0,0 +1,16 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in .config/README.md
*/
module.exports = {
endOfLine: 'auto',
printWidth: 120,
trailingComma: 'es5',
semi: true,
jsxSingleQuote: false,
singleQuote: true,
useTabs: false,
tabWidth: 2,
};

77
enne2corp-fullstacktest-app/.config/Dockerfile

@ -0,0 +1,77 @@
ARG grafana_version=latest
ARG grafana_image=grafana-enterprise
FROM grafana/${grafana_image}:${grafana_version}
ARG anonymous_auth_enabled=true
ARG development=false
ARG TARGETARCH
ARG GO_VERSION=1.21.6
ARG GO_ARCH=${TARGETARCH:-amd64}
ENV DEV "${development}"
# Make it as simple as possible to access the grafana instance for development purposes
# Do NOT enable these settings in a public facing / production grafana instance
ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin"
ENV GF_AUTH_ANONYMOUS_ENABLED "${anonymous_auth_enabled}"
ENV GF_AUTH_BASIC_ENABLED "false"
# Set development mode so plugins can be loaded without the need to sign
ENV GF_DEFAULT_APP_MODE "development"
LABEL maintainer="Grafana Labs <hello@grafana.com>"
ENV GF_PATHS_HOME="/usr/share/grafana"
WORKDIR $GF_PATHS_HOME
USER root
# Installing supervisor and inotify-tools
RUN if [ "${development}" = "true" ]; then \
if grep -i -q alpine /etc/issue; then \
apk add supervisor inotify-tools git; \
elif grep -i -q ubuntu /etc/issue; then \
DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install -y supervisor inotify-tools git && \
rm -rf /var/lib/apt/lists/*; \
else \
echo 'ERROR: Unsupported base image' && /bin/false; \
fi \
fi
COPY supervisord/supervisord.conf /etc/supervisor.d/supervisord.ini
COPY supervisord/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Installing Go
RUN if [ "${development}" = "true" ]; then \
curl -O -L https://golang.org/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \
rm -rf /usr/local/go && \
tar -C /usr/local -xzf go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \
echo "export PATH=$PATH:/usr/local/go/bin:~/go/bin" >> ~/.bashrc && \
rm -f go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \
fi
# Installing delve for debugging
RUN if [ "${development}" = "true" ]; then \
/usr/local/go/bin/go install github.com/go-delve/delve/cmd/dlv@latest; \
fi
# Installing mage for plugin (re)building
RUN if [ "${development}" = "true" ]; then \
git clone https://github.com/magefile/mage; \
cd mage; \
export PATH=$PATH:/usr/local/go/bin; \
go run bootstrap.go; \
fi
# Inject livereload script into grafana index.html
RUN sed -i 's|</body>|<script src="http://localhost:35729/livereload.js"></script></body>|g' /usr/share/grafana/public/views/index.html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

165
enne2corp-fullstacktest-app/.config/README.md

@ -0,0 +1,165 @@
# Default build configuration by Grafana
**This is an auto-generated directory and is not intended to be changed! ⚠**
The `.config/` directory holds basic configuration for the different tools
that are used to develop, test and build the project. In order to make it updates easier we ask you to
not edit files in this folder to extend configuration.
## How to extend the basic configs?
Bear in mind that you are doing it at your own risk, and that extending any of the basic configuration can lead
to issues around working with the project.
### Extending the ESLint config
Edit the `.eslintrc` file in the project root in order to extend the ESLint configuration.
**Example:**
```json
{
"extends": "./.config/.eslintrc",
"rules": {
"react/prop-types": "off"
}
}
```
---
### Extending the Prettier config
Edit the `.prettierrc.js` file in the project root in order to extend the Prettier configuration.
**Example:**
```javascript
module.exports = {
// Prettier configuration provided by Grafana scaffolding
...require('./.config/.prettierrc.js'),
semi: false,
};
```
---
### Extending the Jest config
There are two configuration in the project root that belong to Jest: `jest-setup.js` and `jest.config.js`.
**`jest-setup.js`:** A file that is run before each test file in the suite is executed. We are using it to
set up the Jest DOM for the testing library and to apply some polyfills. ([link to Jest docs](https://jestjs.io/docs/configuration#setupfilesafterenv-array))
**`jest.config.js`:** The main Jest configuration file that extends the Grafana recommended setup. ([link to Jest docs](https://jestjs.io/docs/configuration))
#### ESM errors with Jest
A common issue with the current jest config involves importing an npm package that only offers an ESM build. These packages cause jest to error with `SyntaxError: Cannot use import statement outside a module`. To work around this, we provide a list of known packages to pass to the `[transformIgnorePatterns](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)` jest configuration property. If need be, this can be extended in the following way:
```javascript
process.env.TZ = 'UTC';
const { grafanaESModules, nodeModulesToTransform } = require('./config/jest/utils');
module.exports = {
// Jest configuration provided by Grafana
...require('./.config/jest.config'),
// Inform jest to only transform specific node_module packages.
transformIgnorePatterns: [nodeModulesToTransform([...grafanaESModules, 'packageName'])],
};
```
---
### Extending the TypeScript config
Edit the `tsconfig.json` file in the project root in order to extend the TypeScript configuration.
**Example:**
```json
{
"extends": "./.config/tsconfig.json",
"compilerOptions": {
"preserveConstEnums": true
}
}
```
---
### Extending the Webpack config
Follow these steps to extend the basic Webpack configuration that lives under `.config/`:
#### 1. Create a new Webpack configuration file
Create a new config file that is going to extend the basic one provided by Grafana.
It can live in the project root, e.g. `webpack.config.ts`.
#### 2. Merge the basic config provided by Grafana and your custom setup
We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge) for this.
```typescript
// webpack.config.ts
import type { Configuration } from 'webpack';
import { merge } from 'webpack-merge';
import grafanaConfig, { type Env } from './.config/webpack/webpack.config';
const config = async (env: Env): Promise<Configuration> => {
const baseConfig = await grafanaConfig(env);
return merge(baseConfig, {
// Add custom config here...
output: {
asyncChunks: true,
},
});
};
export default config;
```
#### 3. Update the `package.json` to use the new Webpack config
We need to update the `scripts` in the `package.json` to use the extended Webpack configuration.
**Update for `build`:**
```diff
-"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
+"build": "webpack -c ./webpack.config.ts --env production",
```
**Update for `dev`:**
```diff
-"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
+"dev": "webpack -w -c ./webpack.config.ts --env development",
```
### Configure grafana image to use when running docker
By default, `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behavior, simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`.
**Example:**
```yaml
version: '3.7'
services:
grafana:
extends:
file: .config/docker-compose-base.yaml
service: grafana
build:
args:
grafana_version: ${GRAFANA_VERSION:-9.1.2}
grafana_image: ${GRAFANA_IMAGE:-grafana}
```
In this example, we assign the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will allow you to set the value while running the docker compose commands, which might be convenient in some scenarios.
---

43
enne2corp-fullstacktest-app/.config/bundler/externals.ts

@ -0,0 +1,43 @@
import type { Configuration, ExternalItemFunctionData } from 'webpack';
type ExternalsType = Configuration['externals'];
export const externals: ExternalsType = [
// Required for dynamic publicPath resolution
{ 'amd-module': 'module' },
'lodash',
'jquery',
'moment',
'slate',
'emotion',
'@emotion/react',
'@emotion/css',
'prismjs',
'slate-plain-serializer',
'@grafana/slate-react',
'react',
'react-dom',
'react-redux',
'redux',
'rxjs',
'i18next',
'react-router',
'd3',
'angular',
/^@grafana\/ui/i,
/^@grafana\/runtime/i,
/^@grafana\/data/i,
// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
({ request }: ExternalItemFunctionData, callback: (error?: Error, result?: string) => void) => {
const prefix = 'grafana/';
const hasPrefix = (request: string) => request.indexOf(prefix) === 0;
const stripPrefix = (request: string) => request.slice(prefix.length);
if (request && hasPrefix(request)) {
return callback(undefined, stripPrefix(request));
}
callback();
},
];

31
enne2corp-fullstacktest-app/.config/docker-compose-base.yaml

@ -0,0 +1,31 @@
services:
grafana:
user: root
container_name: 'enne2corp-fullstacktest-app'
build:
context: .
args:
grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}
grafana_version: ${GRAFANA_VERSION:-12.2.0}
development: ${DEVELOPMENT:-false}
anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true}
ports:
- 3000:3000/tcp
- 2345:2345/tcp # delve
security_opt:
- 'apparmor:unconfined'
- 'seccomp:unconfined'
cap_add:
- SYS_PTRACE
volumes:
- ../dist:/var/lib/grafana/plugins/enne2corp-fullstacktest-app
- ../provisioning:/etc/grafana/provisioning
- ..:/root/enne2corp-fullstacktest-app
environment:
NODE_ENV: development
GF_LOG_FILTERS: plugin.enne2corp-fullstacktest-app:debug
GF_LOG_LEVEL: debug
GF_DATAPROXY_LOGGING: 1
GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: enne2corp-fullstacktest-app

18
enne2corp-fullstacktest-app/.config/entrypoint.sh

@ -0,0 +1,18 @@
#!/bin/sh
if [ "${DEV}" = "false" ]; then
echo "Starting test mode"
exec /run.sh
fi
echo "Starting development mode"
if grep -i -q alpine /etc/issue; then
exec /usr/bin/supervisord -c /etc/supervisord.conf
elif grep -i -q ubuntu /etc/issue; then
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
else
echo 'ERROR: Unsupported base image'
exit 1
fi

28
enne2corp-fullstacktest-app/.config/jest-setup.js

@ -0,0 +1,28 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config
*/
import '@testing-library/jest-dom';
import { TextEncoder, TextDecoder } from 'util';
Object.assign(global, { TextDecoder, TextEncoder });
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(global, 'matchMedia', {
writable: true,
value: (query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}),
});
HTMLCanvasElement.prototype.getContext = () => {};

44
enne2corp-fullstacktest-app/.config/jest.config.js

@ -0,0 +1,44 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config
*/
const path = require('path');
const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils');
module.exports = {
moduleNameMapper: {
'\\.(css|scss|sass)$': 'identity-obj-proxy',
'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'),
},
modulePaths: ['<rootDir>/src'],
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
testEnvironment: 'jest-environment-jsdom',
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}',
],
transform: {
'^.+\\.(t|j)sx?$': [
'@swc/jest',
{
sourceMaps: 'inline',
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: false,
dynamicImport: true,
},
},
},
],
},
// Jest will throw `Cannot use import statement outside module` if it tries to load an
// ES module without it being transformed first. ./config/README.md#esm-errors-with-jest
transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)],
watchPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/dist'],
};

25
enne2corp-fullstacktest-app/.config/jest/mocks/react-inlinesvg.tsx

@ -0,0 +1,25 @@
// Due to the grafana/ui Icon component making fetch requests to
// `/public/img/icon/<icon_name>.svg` we need to mock react-inlinesvg to prevent
// the failed fetch requests from displaying errors in console.
import React from 'react';
type Callback = (...args: any[]) => void;
export interface StorageItem {
content: string;
queue: Callback[];
status: string;
}
export const cacheStore: { [key: string]: StorageItem } = Object.create(null);
const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/;
const InlineSVG = ({ src }: { src: string }) => {
// testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`)
const testId = src.replace(SVG_FILE_NAME_REGEX, '$2');
return <svg xmlns="http://www.w3.org/2000/svg" data-testid={testId} viewBox="0 0 24 24" />;
};
export default InlineSVG;

37
enne2corp-fullstacktest-app/.config/jest/utils.js

@ -0,0 +1,37 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in .config/README.md
*/
/*
* This utility function is useful in combination with jest `transformIgnorePatterns` config
* to transform specific packages (e.g.ES modules) in a projects node_modules folder.
*/
const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`;
// Array of known nested grafana package dependencies that only bundle an ESM version
const grafanaESModules = [
'.pnpm', // Support using pnpm symlinked packages
'@grafana/schema',
'@wojtekmaj/date-utils',
'd3',
'd3-color',
'd3-force',
'd3-interpolate',
'd3-scale-chromatic',
'get-user-locale',
'marked',
'memoize',
'mimic-function',
'ol',
'react-calendar',
'react-colorful',
'rxjs',
'uuid',
];
module.exports = {
nodeModulesToTransform,
grafanaESModules,
};

47
enne2corp-fullstacktest-app/.config/supervisord/supervisord.conf

@ -0,0 +1,47 @@
[supervisord]
nodaemon=true
user=root
[program:grafana]
user=root
directory=/var/lib/grafana
command=bash -c 'while [ ! -f /root/enne2corp-fullstacktest-app/dist/gpx_fullstack_test* ]; do sleep 1; done; /run.sh'
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
killasgroup=true
stopasgroup=true
autostart=true
[program:delve]
user=root
command=/bin/bash -c 'pid=""; while [ -z "$pid" ]; do pid=$(pgrep -f gpx_fullstack_test); done; /root/go/bin/dlv attach --api-version=2 --headless --continue --accept-multiclient --listen=:2345 $pid'
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
killasgroup=false
stopasgroup=false
autostart=true
autorestart=true
[program:build-watcher]
user=root
command=/bin/bash -c 'while inotifywait -e modify,create,delete -r /var/lib/grafana/plugins/enne2corp-fullstacktest-app; do echo "Change detected, restarting delve...";supervisorctl restart delve; done'
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
killasgroup=true
stopasgroup=true
autostart=true
[program:mage-watcher]
user=root
environment=PATH="/usr/local/go/bin:/root/go/bin:%(ENV_PATH)s"
directory=/root/enne2corp-fullstacktest-app
command=/bin/bash -c 'git config --global --add safe.directory /root/enne2corp-fullstacktest-app && mage -v watch'
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
killasgroup=true
stopasgroup=true
autostart=true

26
enne2corp-fullstacktest-app/.config/tsconfig.json

@ -0,0 +1,26 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-typescript-config
*/
{
"compilerOptions": {
"alwaysStrict": true,
"declaration": false,
"rootDir": "../src",
"baseUrl": "../src",
"typeRoots": ["../node_modules/@types"],
"resolveJsonModule": true
},
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"esModuleInterop": true
},
"transpileOnly": true
},
"include": ["../src", "./types"],
"extends": "@grafana/tsconfig"
}

37
enne2corp-fullstacktest-app/.config/types/bundler-rules.d.ts vendored

@ -0,0 +1,37 @@
// Image declarations
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
// Font declarations
declare module '*.woff';
declare module '*.woff2';
declare module '*.eot';
declare module '*.ttf';
declare module '*.otf';

83
enne2corp-fullstacktest-app/.config/types/webpack-plugins.d.ts vendored

@ -0,0 +1,83 @@
declare module 'replace-in-file-webpack-plugin' {
import { Compiler, Plugin } from 'webpack';
interface ReplaceRule {
search: string | RegExp;
replace: string | ((match: string) => string);
}
interface ReplaceOption {
dir?: string;
files?: string[];
test?: RegExp | RegExp[];
rules: ReplaceRule[];
}
class ReplaceInFilePlugin extends Plugin {
constructor(options?: ReplaceOption[]);
options: ReplaceOption[];
apply(compiler: Compiler): void;
}
export = ReplaceInFilePlugin;
}
declare module 'webpack-livereload-plugin' {
import { ServerOptions } from 'https';
import { Compiler, Plugin, Stats, Compilation } from 'webpack';
interface Options extends Pick<ServerOptions, 'cert' | 'key' | 'pfx'> {
/**
* protocol for livereload `<script>` src attribute value
* @default protocol of the page, either `http` or `https`
*/
protocol?: string | undefined;
/**
* The desired port for the livereload server.
* If you define port 0, an available port will be searched for, starting from 35729.
* @default 35729
*/
port?: number | undefined;
/**
* he desired hostname for the appended `<script>` (if present) to point to
* @default hostname of the page, like `localhost` or 10.0.2.2
*/
hostname?: string | undefined;
/**
* livereload `<script>` automatically to `<head>`.
* @default false
*/
appendScriptTag?: boolean | undefined;
/**
* RegExp of files to ignore. Null value means ignore nothing.
* It is also possible to define an array and use multiple anymatch patterns
*/
ignore?: RegExp | RegExp[] | null | undefined;
/**
* amount of milliseconds by which to delay the live reload (in case build takes longer)
* @default 0
*/
delay?: number | undefined;
/**
* create hash for each file source and only notify livereload if hash has changed
* @default false
*/
useSourceHash?: boolean | undefined;
}
class LiveReloadPlugin extends Plugin {
readonly isRunning: boolean;
constructor(options?: Options);
apply(compiler: Compiler): void;
start(watching: any, cb: () => void): void;
done(stats: Stats): void;
failed(): void;
autoloadJs(): string;
scriptTag(source: string): string;
applyCompilation(compilation: Compilation): void;
}
export = LiveReloadPlugin;
}

33
enne2corp-fullstacktest-app/.config/webpack/BuildModeWebpackPlugin.ts

@ -0,0 +1,33 @@
import webpack, { type Compiler } from 'webpack';
const PLUGIN_NAME = 'BuildModeWebpack';
export class BuildModeWebpackPlugin {
apply(compiler: webpack.Compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
async () => {
const assets = compilation.getAssets();
for (const asset of assets) {
if (asset.name.endsWith('plugin.json')) {
const pluginJsonString = asset.source.source().toString();
const pluginJsonWithBuildMode = JSON.stringify(
{
...JSON.parse(pluginJsonString),
buildMode: compilation.options.mode,
},
null,
4
);
compilation.updateAsset(asset.name, new webpack.sources.RawSource(pluginJsonWithBuildMode));
}
}
}
);
});
}
}

2
enne2corp-fullstacktest-app/.config/webpack/constants.ts

@ -0,0 +1,2 @@
export const SOURCE_DIR = 'src';
export const DIST_DIR = 'dist';

68
enne2corp-fullstacktest-app/.config/webpack/utils.ts

@ -0,0 +1,68 @@
import fs from 'fs';
import process from 'process';
import os from 'os';
import path from 'path';
import { glob } from 'glob';
import { SOURCE_DIR } from './constants.ts';
export function isWSL() {
if (process.platform !== 'linux') {
return false;
}
if (os.release().toLowerCase().includes('microsoft')) {
return true;
}
try {
return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft');
} catch {
return false;
}
}
function loadJson(path: string) {
const rawJson = fs.readFileSync(path, 'utf8');
return JSON.parse(rawJson);
}
export function getPackageJson() {
return loadJson(path.resolve(process.cwd(), 'package.json'));
}
export function getPluginJson() {
return loadJson(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`));
}
export function getCPConfigVersion() {
const cprcJson = path.resolve(process.cwd(), './.config', '.cprc.json');
return fs.existsSync(cprcJson) ? loadJson(cprcJson).version : { version: 'unknown' };
}
export function hasReadme() {
return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md'));
}
// Support bundling nested plugins by finding all plugin.json files in src directory
// then checking for a sibling module.[jt]sx? file.
export async function getEntries() {
const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true });
const plugins = await Promise.all(
pluginsJson.map((pluginJson) => {
const folder = path.dirname(pluginJson);
return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true });
})
);
return plugins.reduce<Record<string, string>>((result, modules) => {
return modules.reduce((innerResult, module) => {
const pluginPath = path.dirname(module);
const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, '');
const entryName = pluginName === '' ? 'module' : `${pluginName}/module`;
innerResult[entryName] = module;
return innerResult;
}, result);
}, {});
}

247
enne2corp-fullstacktest-app/.config/webpack/webpack.config.ts

@ -0,0 +1,247 @@
/*
* THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY.
*
* In order to extend the configuration follow the steps in
* https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config
*/
import CopyWebpackPlugin from 'copy-webpack-plugin';
import ESLintPlugin from 'eslint-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import path from 'path';
import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
import webpack, { type Configuration } from 'webpack';
import LiveReloadPlugin from 'webpack-livereload-plugin';
import VirtualModulesPlugin from 'webpack-virtual-modules';
import { BuildModeWebpackPlugin } from './BuildModeWebpackPlugin.ts';
import { DIST_DIR, SOURCE_DIR } from './constants.ts';
import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils.ts';
import { externals } from '../bundler/externals.ts';
const pluginJson = getPluginJson();
const cpVersion = getCPConfigVersion();
const pluginVersion = getPackageJson().version;
const virtualPublicPath = new VirtualModulesPlugin({
'node_modules/grafana-public-path.js': `
import amdMetaModule from 'amd-module';
__webpack_public_path__ =
amdMetaModule && amdMetaModule.uri
? amdMetaModule.uri.slice(0, amdMetaModule.uri.lastIndexOf('/') + 1)
: 'public/plugins/${pluginJson.id}/';
`,
});
export type Env = {
[key: string]: true | string | Env;
};
const config = async (env: Env): Promise<Configuration> => {
const baseConfig: Configuration = {
cache: {
type: 'filesystem',
buildDependencies: {
// __filename doesn't work in Node 24
config: [path.resolve(process.cwd(), '.config', 'webpack', 'webpack.config.ts')],
},
},
context: path.join(process.cwd(), SOURCE_DIR),
devtool: env.production ? 'source-map' : 'eval-source-map',
entry: await getEntries(),
externals,
// Support WebAssembly according to latest spec - makes WebAssembly module async
experiments: {
asyncWebAssembly: true,
},
mode: env.production ? 'production' : 'development',
module: {
rules: [
// This must come first in the rules array otherwise it breaks sourcemaps.
{
test: /src\/(?:.*\/)?module\.tsx?$/,
use: [
{
loader: 'imports-loader',
options: {
imports: `side-effects grafana-public-path`,
},
},
],
},
{
exclude: /(node_modules)/,
test: /\.[tj]sx?$/,
use: {
loader: 'swc-loader',
options: {
jsc: {
baseUrl: path.resolve(process.cwd(), SOURCE_DIR),
target: 'es2015',
loose: false,
parser: {
syntax: 'typescript',
tsx: true,
decorators: false,
dynamicImport: true,
},
},
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset/resource',
generator: {
filename: Boolean(env.production) ? '[hash][ext]' : '[file]',
},
},
{
test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/,
type: 'asset/resource',
generator: {
filename: Boolean(env.production) ? '[hash][ext]' : '[file]',
},
},
],
},
optimization: {
minimize: Boolean(env.production),
minimizer: [
new TerserPlugin({
terserOptions: {
format: {
comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'),
},
compress: {
drop_console: ['log', 'info'],
},
},
}),
],
},
output: {
clean: {
keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`),
},
filename: '[name].js',
chunkFilename: env.production ? '[name].js?_cache=[contenthash]' : '[name].js',
library: {
type: 'amd',
},
path: path.resolve(process.cwd(), DIST_DIR),
publicPath: `public/plugins/${pluginJson.id}/`,
uniqueName: pluginJson.id,
crossOriginLoading: 'anonymous',
},
plugins: [
new BuildModeWebpackPlugin(),
virtualPublicPath,
// Insert create plugin version information into the bundle
new webpack.BannerPlugin({
banner: `/* [create-plugin] version: ${cpVersion} */
/* [create-plugin] plugin: ${pluginJson.id}@${pluginVersion} */`,
raw: true,
entryOnly: true,
}),
new CopyWebpackPlugin({
patterns: [
// If src/README.md exists use it; otherwise the root README
// To `compiler.options.output`
{ from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true },
{ from: 'plugin.json', to: '.' },
{ from: '../LICENSE', to: '.' },
{ from: '../CHANGELOG.md', to: '.', force: true },
{ from: '**/*.json', to: '.' },
{ from: '**/*.svg', to: '.', noErrorOnMissing: true },
{ from: '**/*.png', to: '.', noErrorOnMissing: true },
{ from: '**/*.html', to: '.', noErrorOnMissing: true },
{ from: 'img/**/*', to: '.', noErrorOnMissing: true },
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true },
{ from: 'static/**/*', to: '.', noErrorOnMissing: true },
{ from: '**/query_help.md', to: '.', noErrorOnMissing: true },
],
}),
// Replace certain template-variables in the README and plugin.json
new ReplaceInFileWebpackPlugin([
{
dir: DIST_DIR,
files: ['plugin.json', 'README.md'],
rules: [
{
search: /\%VERSION\%/g,
replace: pluginVersion,
},
{
search: /\%TODAY\%/g,
replace: new Date().toISOString().substring(0, 10),
},
{
search: /\%PLUGIN_ID\%/g,
replace: pluginJson.id,
},
],
},
]),
new SubresourceIntegrityPlugin({
hashFuncNames: ['sha256'],
}),
...(env.development
? [
new LiveReloadPlugin(),
new ForkTsCheckerWebpackPlugin({
async: Boolean(env.development),
issue: {
include: [{ file: '**/*.{ts,tsx}' }],
},
typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') },
}),
new ESLintPlugin({
extensions: ['.ts', '.tsx'],
lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files
failOnError: Boolean(env.production),
}),
]
: []),
],
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
// handle resolving "rootDir" paths
modules: [path.resolve(process.cwd(), 'src'), 'node_modules'],
unsafeCache: true,
},
};
if (isWSL()) {
baseConfig.watchOptions = {
poll: 3000,
ignored: /node_modules/,
};
}
return baseConfig;
};
export default config;

7
enne2corp-fullstacktest-app/.cprc.json

@ -0,0 +1,7 @@
{
"features": {
"bundleGrafanaUI": false,
"useReactRouterV6": true,
"useExperimentalRspack": false
}
}

3
enne2corp-fullstacktest-app/.eslintrc

@ -0,0 +1,3 @@
{
"extends": "./.config/.eslintrc"
}

27
enne2corp-fullstacktest-app/.github/workflows/bundle-stats.yml

@ -0,0 +1,27 @@
name: Bundle Stats
on:
workflow_dispatch:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: write
pull-requests: write
actions: read
jobs:
compare:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
- uses: grafana/plugin-actions/bundle-size@main # zizmor: ignore[unpinned-uses] provided by grafana

241
enne2corp-fullstacktest-app/.github/workflows/ci.yml

@ -0,0 +1,241 @@
name: CI
on:
push:
branches:
- master
- main
pull_request:
branches:
- master
- main
jobs:
build:
name: Build, lint and unit tests
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
plugin-id: ${{ steps.metadata.outputs.plugin-id }}
plugin-version: ${{ steps.metadata.outputs.plugin-version }}
has-e2e: ${{ steps.check-for-e2e.outputs.has-e2e }}
has-backend: ${{ steps.check-for-backend.outputs.has-backend }}
env:
GRAFANA_ACCESS_POLICY_TOKEN: ${{ secrets.GRAFANA_ACCESS_POLICY_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check types
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Unit tests
run: npm run test:ci
- name: Build frontend
run: npm run build
- name: Check for backend
id: check-for-backend
run: |
if [ -f "Magefile.go" ]
then
echo "has-backend=true" >> $GITHUB_OUTPUT
fi
- name: Setup Go environment
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Test backend
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3.1.0
with:
version: latest
args: coverage
- name: Build backend
if: steps.check-for-backend.outputs.has-backend == 'true'
uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3.1.0
with:
version: latest
args: buildAll
- name: Check for E2E
id: check-for-e2e
run: |
if [ -f "playwright.config.ts" ]
then
echo "has-e2e=true" >> $GITHUB_OUTPUT
fi
- name: Sign plugin
run: npm run sign
if: ${{ env.GRAFANA_ACCESS_POLICY_TOKEN != '' }}
- name: Get plugin metadata
id: metadata
run: |
sudo apt-get install jq
export GRAFANA_PLUGIN_ID=$(cat dist/plugin.json | jq -r .id)
export GRAFANA_PLUGIN_VERSION=$(cat dist/plugin.json | jq -r .info.version)
export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip
echo "plugin-id=${GRAFANA_PLUGIN_ID}" >> $GITHUB_OUTPUT
echo "plugin-version=${GRAFANA_PLUGIN_VERSION}" >> $GITHUB_OUTPUT
echo "archive=${GRAFANA_PLUGIN_ARTIFACT}" >> $GITHUB_OUTPUT
- name: Package plugin
id: package-plugin
run: |
mv dist ${PLUGIN_ID}
zip ${ARCHIVE} ${PLUGIN_ID} -r
env:
ARCHIVE: ${{ steps.metadata.outputs.archive }}
PLUGIN_ID: ${{ steps.metadata.outputs.plugin-id }}
- name: Check plugin.json
run: |
docker run --pull=always \
-v $PWD/${ARCHIVE}:/archive.zip \
grafana/plugin-validator-cli -analyzer=metadatavalid /archive.zip
env:
ARCHIVE: ${{ steps.metadata.outputs.archive }}
- name: Archive Build
uses: actions/upload-artifact@v4
with:
name: ${{ steps.metadata.outputs.plugin-id }}-${{ steps.metadata.outputs.plugin-version }}
path: ${{ steps.metadata.outputs.plugin-id }}
retention-days: 5
resolve-versions:
name: Resolve e2e images
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 3
needs: build
if: ${{ needs.build.outputs.has-e2e == 'true' }}
outputs:
matrix: ${{ steps.resolve-versions.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Resolve Grafana E2E versions
id: resolve-versions
uses: grafana/plugin-actions/e2e-version@main # zizmor: ignore[unpinned-uses] provided by grafana
playwright-tests:
needs: [resolve-versions, build]
timeout-minutes: 15
permissions:
contents: read
id-token: write
pull-requests: write
strategy:
fail-fast: false
matrix:
GRAFANA_IMAGE: ${{fromJson(needs.resolve-versions.outputs.matrix)}}
name: e2e test ${{ matrix.GRAFANA_IMAGE.name }}@${{ matrix.GRAFANA_IMAGE.VERSION }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Download plugin
uses: actions/download-artifact@v4
with:
path: dist
name: ${{ needs.build.outputs.plugin-id }}-${{ needs.build.outputs.plugin-version }}
- name: Execute permissions on binary
if: needs.build.outputs.has-backend == 'true'
run: |
chmod +x ./dist/gpx_*
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dev dependencies
run: npm ci
- name: Start Grafana
run: |
docker compose pull
ANONYMOUS_AUTH_ENABLED=false DEVELOPMENT=false GRAFANA_VERSION=${{ matrix.GRAFANA_IMAGE.VERSION }} GRAFANA_IMAGE=${{ matrix.GRAFANA_IMAGE.NAME }} docker compose up -d
- name: Wait for grafana server
uses: grafana/plugin-actions/wait-for-grafana@main # zizmor: ignore[unpinned-uses] provided by grafana
with:
url: http://localhost:3000/login
- name: Install Playwright Browsers
run: npm exec playwright install chromium --with-deps
- name: Run Playwright tests
id: run-tests
run: npm run e2e
- name: Upload e2e test summary
uses: grafana/plugin-actions/playwright-gh-pages/upload-report-artifacts@main # zizmor: ignore[unpinned-uses] provided by grafana
if: ${{ always() && !cancelled() }}
with:
upload-report: false
github-token: ${{ secrets.GITHUB_TOKEN }}
test-outcome: ${{ steps.run-tests.outcome }}
- name: Docker logs
if: ${{ always() && steps.run-tests.outcome == 'failure' }}
run: |
docker logs enne2corp-fullstacktest-app >& grafana-server.log
- name: Stop grafana docker
run: docker compose down
# Uncomment this step to upload the server log to Github artifacts. Remember Github artifacts are public on the Internet if the repository is public.
# - name: Upload server log
# uses: actions/upload-artifact@v4
# if: ${{ always() && steps.run-tests.outcome == 'failure' }}
# with:
# name: ${{ matrix.GRAFANA_IMAGE.NAME }}-v${{ matrix.GRAFANA_IMAGE.VERSION }}-${{github.run_id}}-server-log
# path: grafana-server.log
# retention-days: 5
publish-report:
if: ${{ always() && !cancelled() }}
permissions:
contents: write
id-token: write
pull-requests: write
needs: [playwright-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# required for playwright-gh-pages
persist-credentials: true
- name: Publish report
uses: grafana/plugin-actions/playwright-gh-pages/deploy-report-pages@main # zizmor: ignore[unpinned-uses] provided by grafana
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

26
enne2corp-fullstacktest-app/.github/workflows/cp-update.yml

@ -0,0 +1,26 @@
name: Create Plugin Update
on:
workflow_dispatch:
schedule:
- cron: '0 0 1 * *' # run once a month on the 1st day
# To use the default github token with the following elevated permissions make sure to check:
# **Allow GitHub Actions to create and approve pull requests** in https://github.com/ORG_NAME/REPO_NAME/settings/actions.
# Alternatively create a fine-grained personal access token for your repository with
# `contents: read and write` and `pull requests: read and write` and pass it to the action.
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: grafana/plugin-actions/create-plugin-update@main # zizmor: ignore[unpinned-uses] provided by grafana
# Uncomment to use a fine-grained personal access token instead of default github token
# (For more info on how to generate the token see https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
# with:
# Make sure to save the token in your repository secrets
# token: $

37
enne2corp-fullstacktest-app/.github/workflows/is-compatible.yml

@ -0,0 +1,37 @@
name: Latest Grafana API compatibility check
on: [pull_request]
jobs:
compatibilitycheck:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Node.js environment
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npm run build
- name: Find module.ts or module.tsx
id: find-module-ts
run: |
MODULETS="$(find ./src -type f \( -name "module.ts" -o -name "module.tsx" \))"
echo "modulets=${MODULETS}" >> $GITHUB_OUTPUT
- name: Compatibility check
uses: grafana/plugin-actions/is-compatible@main
with:
module: ${{ steps.find-module-ts.outputs.modulets }}
comment-pr: 'no'
fail-if-incompatible: 'yes'

29
enne2corp-fullstacktest-app/.github/workflows/release.yml

@ -0,0 +1,29 @@
# This GitHub Action automates the process of building Grafana plugins.
# (For more information, see https://github.com/grafana/plugin-actions/blob/main/build-plugin/README.md)
name: Release
on:
push:
tags:
- 'v*' # Run workflow on version tags, e.g. v1.0.0.
permissions: read-all
jobs:
release:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: grafana/plugin-actions/build-plugin@main # zizmor: ignore[unpinned-uses] provided by grafana
# Uncomment to enable plugin signing
# (For more info on how to generate the access policy token see https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin#generate-an-access-policy-token)
#with:
# Make sure to save the token in your repository secrets
#policy_token: $
# Usage of GRAFANA_API_KEY is deprecated, prefer `policy_token` option above
#grafana_token: $

46
enne2corp-fullstacktest-app/.gitignore vendored

@ -0,0 +1,46 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
node_modules/
# yarn
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Compiled binary addons (https://nodejs.org/api/addons.html)
dist/
artifacts/
work/
ci/
# e2e test directories
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
# Editor
.idea
.eslintcache

1
enne2corp-fullstacktest-app/.nvmrc

@ -0,0 +1 @@
22

4
enne2corp-fullstacktest-app/.prettierrc.js

@ -0,0 +1,4 @@
module.exports = {
// Prettier configuration provided by Grafana scaffolding
...require('./.config/.prettierrc.js'),
};

5
enne2corp-fullstacktest-app/CHANGELOG.md

@ -0,0 +1,5 @@
# Changelog
## 1.0.0 (Unreleased)
Initial release.

201
enne2corp-fullstacktest-app/LICENSE

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

12
enne2corp-fullstacktest-app/Magefile.go

@ -0,0 +1,12 @@
//go:build mage
// +build mage
package main
import (
// mage:import
build "github.com/grafana/grafana-plugin-sdk-go/build"
)
// Default configures the default target.
var Default = build.BuildAll

136
enne2corp-fullstacktest-app/README.md

@ -0,0 +1,136 @@
# Grafana app plugin template
This template is a starting point for building an app plugin for Grafana.
## What are Grafana app plugins?
App plugins can let you create a custom out-of-the-box monitoring experience by custom pages, nested data sources and panel plugins.
## Get started
### Backend
1. Update [Grafana plugin SDK for Go](https://grafana.com/developers/plugin-tools/key-concepts/backend-plugins/grafana-plugin-sdk-for-go) dependency to the latest minor version:
```bash
go get -u github.com/grafana/grafana-plugin-sdk-go
go mod tidy
```
2. Build plugin backend binaries for Linux, Windows and Darwin:
```bash
mage -v
```
3. List all available Mage targets for additional commands:
```bash
mage -l
```
### Frontend
1. Install dependencies
```bash
npm install
```
2. Build plugin in development mode and run in watch mode
```bash
npm run dev
```
3. Build plugin in production mode
```bash
npm run build
```
4. Run the tests (using Jest)
```bash
# Runs the tests and watches for changes, requires git init first
npm run test
# Exits after running all the tests
npm run test:ci
```
5. Spin up a Grafana instance and run the plugin inside it (using Docker)
```bash
npm run server
```
6. Run the E2E tests (using Playwright)
```bash
# Spins up a Grafana instance first that we tests against
npm run server
# If you wish to start a certain Grafana version. If not specified will use latest by default
GRAFANA_VERSION=11.3.0 npm run server
# Starts the tests
npm run e2e
```
7. Run the linter
```bash
npm run lint
# or
npm run lint:fix
```
# Distributing your plugin
When distributing a Grafana plugin either within the community or privately the plugin must be signed so the Grafana application can verify its authenticity. This can be done with the `@grafana/sign-plugin` package.
_Note: It's not necessary to sign a plugin during development. The docker development environment that is scaffolded with `@grafana/create-plugin` caters for running the plugin without a signature._
## Initial steps
Before signing a plugin please read the Grafana [plugin publishing and signing criteria](https://grafana.com/legal/plugins/#plugin-publishing-and-signing-criteria) documentation carefully.
`@grafana/create-plugin` has added the necessary commands and workflows to make signing and distributing a plugin via the grafana plugins catalog as straightforward as possible.
Before signing a plugin for the first time please consult the Grafana [plugin signature levels](https://grafana.com/legal/plugins/#what-are-the-different-classifications-of-plugins) documentation to understand the differences between the types of signature level.
1. Create a [Grafana Cloud account](https://grafana.com/signup).
2. Make sure that the first part of the plugin ID matches the slug of your Grafana Cloud account.
- _You can find the plugin ID in the `plugin.json` file inside your plugin directory. For example, if your account slug is `acmecorp`, you need to prefix the plugin ID with `acmecorp-`._
3. Create a Grafana Cloud API key with the `PluginPublisher` role.
4. Keep a record of this API key as it will be required for signing a plugin
## Signing a plugin
### Using Github actions release workflow
If the plugin is using the github actions supplied with `@grafana/create-plugin` signing a plugin is included out of the box. The [release workflow](./.github/workflows/release.yml) can prepare everything to make submitting your plugin to Grafana as easy as possible. Before being able to sign the plugin however a secret needs adding to the Github repository.
1. Please navigate to "settings > secrets > actions" within your repo to create secrets.
2. Click "New repository secret"
3. Name the secret "GRAFANA_API_KEY"
4. Paste your Grafana Cloud API key in the Secret field
5. Click "Add secret"
#### Push a version tag
To trigger the workflow we need to push a version tag to github. This can be achieved with the following steps:
1. Run `npm version <major|minor|patch>`
2. Run `git push origin main --follow-tags`
## Learn more
Below you can find source code for existing app plugins and other related documentation.
- [Basic app plugin example](https://github.com/grafana/grafana-plugin-examples/tree/master/examples/app-basic#readme)
- [`plugin.json` documentation](https://grafana.com/developers/plugin-tools/reference/plugin-jsonplugin-json)
- [Sign a plugin](https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin)

5
enne2corp-fullstacktest-app/docker-compose.yaml

@ -0,0 +1,5 @@
services:
grafana:
extends:
file: .config/docker-compose-base.yaml
service: grafana

66
enne2corp-fullstacktest-app/go.mod

@ -0,0 +1,66 @@
module github.com/enne2-corp/fullstack-test
go 1.22
require github.com/grafana/grafana-plugin-sdk-go v0.156.0
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/getkin/kin-openapi v0.94.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v2.0.0+incompatible // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/hashicorp/go-hclog v0.14.1 // indirect
github.com/hashicorp/go-plugin v1.4.3 // indirect
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.1 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.40.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect
github.com/urfave/cli v1.22.12 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79 // indirect
google.golang.org/grpc v1.41.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

405
enne2corp-fullstacktest-app/go.sum

@ -0,0 +1,405 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ=
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2 h1:XCdvHbz3LhewBHN7+mQPx0sg/Hxil/1USnBmxkjHcmY=
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac h1:XDAn206aIqKPdF5YczuuJXSQPx+WOen0Pxbxp5Fq8Pg=
github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac h1:9yrT5tmn9Zc0ytWPASlaPwQfQMQYnRf0RSDe1XvHw0Q=
github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo=
github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI=
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grafana/grafana-plugin-sdk-go v0.156.0 h1:LPR5gpROmdwkkvm5oGihl0mSKyWNHWIjAjzzTxk3k7M=
github.com/grafana/grafana-plugin-sdk-go v0.156.0/go.mod h1:Wj4WD+sfoUB/q5UG016IvUQBM7HBJaNEAc88b++4szs=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU=
github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4=
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.40.0 h1:Afz7EVRqGg2Mqqf4JuF9vdvp1pi220m55Pi9T2JnO4Q=
github.com/prometheus/common v0.40.0/go.mod h1:L65ZJPSmfn/UBWLQIHV7dBrKFidB/wPlF1y5TlSt9OE=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI=
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8/go.mod h1:fVle4kNr08ydeohzYafr20oZzbAkhQT39gKK/pFQ5M4=
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 h1:4EYQaWAatQokdji3zqZloVIW/Ke1RQjYw2zHULyrHJg=
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3/go.mod h1:1xEUf2abjfP92w2GZTV+GgaRxXErwRXcClbUwrNJffU=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191020152052-9984515f0562/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79 h1:s1jFTXJryg4a1mew7xv03VZD8N9XjxFhk1o4Js4WvPQ=
google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

2
enne2corp-fullstacktest-app/jest-setup.js

@ -0,0 +1,2 @@
// Jest setup provided by Grafana scaffolding
import './.config/jest-setup';

8
enne2corp-fullstacktest-app/jest.config.js

@ -0,0 +1,8 @@
// force timezone to UTC to allow tests to work regardless of local timezone
// generally used by snapshots, but can affect specific tests
process.env.TZ = 'UTC';
module.exports = {
// Jest configuration provided by Grafana scaffolding
...require('./.config/jest.config'),
};

14979
enne2corp-fullstacktest-app/package-lock.json generated

File diff suppressed because it is too large Load Diff

80
enne2corp-fullstacktest-app/package.json

@ -0,0 +1,80 @@
{
"name": "fullstack-test",
"version": "1.0.0",
"scripts": {
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
"dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
"test": "jest --watch --onlyChanged",
"test:ci": "jest --passWithNoTests --maxWorkers 4",
"typecheck": "tsc --noEmit",
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
"lint:fix": "npm run lint -- --fix && prettier --write --list-different .",
"e2e": "playwright test",
"server": "docker compose up --build",
"sign": "npx --yes @grafana/sign-plugin@latest"
},
"author": "Enne2 corp",
"license": "Apache-2.0",
"devDependencies": {
"@grafana/eslint-config": "^8.2.0",
"@grafana/plugin-e2e": "^2.1.13",
"@grafana/tsconfig": "^2.0.0",
"@playwright/test": "^1.52.0",
"@stylistic/eslint-plugin-ts": "^2.9.0",
"@swc/core": "^1.3.90",
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.26",
"@testing-library/jest-dom": "6.1.4",
"@testing-library/react": "14.0.0",
"@types/jest": "^29.5.0",
"@types/node": "^20.8.7",
"@types/testing-library__jest-dom": "5.14.8",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"eslint": "^8.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-jsdoc": "^46.8.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-webpack-plugin": "^4.0.1",
"fork-ts-checker-webpack-plugin": "^8.0.0",
"glob": "^10.2.7",
"identity-obj-proxy": "3.0.0",
"imports-loader": "^5.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.8.7",
"replace-in-file-webpack-plugin": "^1.0.6",
"sass": "1.63.2",
"sass-loader": "13.3.1",
"semver": "^7.6.3",
"style-loader": "3.3.3",
"swc-loader": "^0.2.3",
"terser-webpack-plugin": "^5.3.10",
"ts-node": "^10.9.2",
"typescript": "5.5.4",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-livereload-plugin": "^3.0.2",
"webpack-subresource-integrity": "^5.1.0",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=22"
},
"dependencies": {
"@emotion/css": "11.10.6",
"@grafana/data": "^12.2.0",
"@grafana/i18n": "^12.2.0",
"@grafana/runtime": "^12.2.0",
"@grafana/ui": "^12.2.0",
"@grafana/schema": "^12.2.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "^6.22.0",
"rxjs": "7.8.2"
},
"packageManager": "npm@9.2.0"
}

53
enne2corp-fullstacktest-app/playwright.config.ts

@ -0,0 +1,53 @@
import type { PluginOptions } from '@grafana/plugin-e2e';
import { defineConfig, devices } from '@playwright/test';
import { dirname } from 'node:path';
const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`;
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig<PluginOptions>({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.GRAFANA_URL || 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
// 1. Login to Grafana and store the cookie on disk for use in other tests.
{
name: 'auth',
testDir: pluginE2eAuth,
testMatch: [/.*\.js/],
},
// 2. Run tests in Google Chrome. Every test will start authenticated as admin user.
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['auth'],
},
],
});

4
enne2corp-fullstacktest-app/provisioning/plugins/README.md

@ -0,0 +1,4 @@
For more information see our documentation:
- [Provision Grafana](https://grafana.com/docs/grafana/latest/administration/provisioning/)
- [Provision a plugin](https://grafana.com/developers/plugin-tools/publish-a-plugin/provide-test-environment)

11
enne2corp-fullstacktest-app/provisioning/plugins/apps.yaml

@ -0,0 +1,11 @@
apiVersion: 1
apps:
- type: 'enne2corp-fullstacktest-app'
org_id: 1
org_name: 'enne2 corp'
disabled: false
jsonData:
apiUrl: http://default-url.com
secureJsonData:
apiKey: secret-key

50
enne2corp-fullstacktest-app/src/README.md

@ -0,0 +1,50 @@
<!-- This README file is going to be the one displayed on the Grafana.com website for your plugin. Uncomment and replace the content here before publishing.
Remove any remaining comments before publishing as these may be displayed on Grafana.com -->
# Fullstack-Test
<!-- To help maximize the impact of your README and improve usability for users, we propose the following loose structure:
**BEFORE YOU BEGIN**
- Ensure all links are absolute URLs so that they will work when the README is displayed within Grafana and Grafana.com
- Be inspired ✨
- [grafana-polystat-panel](https://github.com/grafana/grafana-polystat-panel)
- [volkovlabs-variable-panel](https://github.com/volkovlabs/volkovlabs-variable-panel)
**ADD SOME BADGES**
Badges convey useful information at a glance for users whether in the Catalog or viewing the source code. You can use the generator on [Shields.io](https://shields.io/badges/dynamic-json-badge) together with the Grafana.com API
to create dynamic badges that update automatically when you publish a new version to the marketplace.
- For the URL parameter use `https://grafana.com/api/plugins/your-plugin-id`.
- Example queries:
- Downloads: `$.downloads`
- Catalog Version: `$.version`
- Grafana Dependency: `$.grafanaDependency`
- Signature Type: `$.versionSignatureType`
- Optionally, for the logo parameter use `grafana`.
Full example: ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?logo=grafana&query=$.version&url=https://grafana.com/api/plugins/grafana-polystat-panel&label=Marketplace&prefix=v&color=F47A20)
Consider other [badges](https://shields.io/badges) as you feel appropriate for your project.
## Overview / Introduction
Provide one or more paragraphs as an introduction to your plugin to help users understand why they should use it.
Consider including screenshots:
- in [plugin.json](https://grafana.com/developers/plugin-tools/reference/plugin-json#info) include them as relative links.
- in the README ensure they are absolute URLs.
## Requirements
List any requirements or dependencies they may need to run the plugin.
## Getting Started
Provide a quick start on how to configure and use the plugin.
## Documentation
If your project has dedicated documentation available for users, provide links here. For help in following Grafana's style recommendations for technical documentation, refer to our [Writer's Toolkit](https://grafana.com/docs/writers-toolkit/).
## Contributing
Do you want folks to contribute to the plugin or provide feedback through specific means? If so, tell them how!
-->

38
enne2corp-fullstacktest-app/src/components/App/App.test.tsx

@ -0,0 +1,38 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { AppRootProps, PluginType } from '@grafana/data';
import { render, waitFor } from '@testing-library/react';
import App from './App';
describe('Components/App', () => {
let props: AppRootProps;
beforeEach(() => {
jest.resetAllMocks();
props = {
basename: 'a/sample-app',
meta: {
id: 'sample-app',
name: 'Sample App',
type: PluginType.app,
enabled: true,
jsonData: {},
},
query: {},
path: '',
onNavChanged: jest.fn(),
} as unknown as AppRootProps;
});
test('renders without an error"', async () => {
const { queryByText } = render(
<MemoryRouter>
<App {...props} />
</MemoryRouter>
);
// Application is lazy loaded, so we need to wait for the component and routes to be rendered
await waitFor(() => expect(queryByText(/this is page one./i)).toBeInTheDocument(), { timeout: 2000 });
});
});

25
enne2corp-fullstacktest-app/src/components/App/App.tsx

@ -0,0 +1,25 @@
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { AppRootProps } from '@grafana/data';
import { ROUTES } from '../../constants';
const PageOne = React.lazy(() => import('../../pages/PageOne'));
const PageTwo = React.lazy(() => import('../../pages/PageTwo'));
const PageThree = React.lazy(() => import('../../pages/PageThree'));
const PageFour = React.lazy(() => import('../../pages/PageFour'));
function App(props: AppRootProps) {
return (
<Routes>
<Route path={ROUTES.Two} element={<PageTwo />} />
<Route path={`${ROUTES.Three}/:id?`} element={<PageThree />} />
{/* Full-width page (this page will have no side navigation) */}
<Route path={ROUTES.Four} element={<PageFour />} />
{/* Default page */}
<Route path="*" element={<PageOne />} />
</Routes>
);
}
export default App;

38
enne2corp-fullstacktest-app/src/components/AppConfig/AppConfig.test.tsx

@ -0,0 +1,38 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { PluginType } from '@grafana/data';
import AppConfig, { AppConfigProps } from './AppConfig';
import { testIds } from 'components/testIds';
describe('Components/AppConfig', () => {
let props: AppConfigProps;
beforeEach(() => {
jest.resetAllMocks();
props = {
plugin: {
meta: {
id: 'sample-app',
name: 'Sample App',
type: PluginType.app,
enabled: true,
jsonData: {},
},
},
query: {},
} as unknown as AppConfigProps;
});
test('renders the "API Settings" fieldset with API key, API url inputs and button', () => {
const plugin = { meta: { ...props.plugin.meta, enabled: false } };
// @ts-ignore - We don't need to provide `addConfigPage()` and `setChannelSupport()` for these tests
render(<AppConfig plugin={plugin} query={props.query} />);
expect(screen.queryByRole('group', { name: /api settings/i })).toBeInTheDocument();
expect(screen.queryByTestId(testIds.appConfig.apiKey)).toBeInTheDocument();
expect(screen.queryByTestId(testIds.appConfig.apiUrl)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /save api settings/i })).toBeInTheDocument();
});
});

140
enne2corp-fullstacktest-app/src/components/AppConfig/AppConfig.tsx

@ -0,0 +1,140 @@
import React, { ChangeEvent, useState } from 'react';
import { lastValueFrom } from 'rxjs';
import { css } from '@emotion/css';
import { AppPluginMeta, GrafanaTheme2, PluginConfigPageProps, PluginMeta } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Button, Field, FieldSet, Input, SecretInput, useStyles2 } from '@grafana/ui';
import { testIds } from '../testIds';
type AppPluginSettings = {
apiUrl?: string;
};
type State = {
// The URL to reach our custom API.
apiUrl: string;
// Tells us if the API key secret is set.
isApiKeySet: boolean;
// A secret key for our custom API.
apiKey: string;
};
export interface AppConfigProps extends PluginConfigPageProps<AppPluginMeta<AppPluginSettings>> {}
const AppConfig = ({ plugin }: AppConfigProps) => {
const s = useStyles2(getStyles);
const { enabled, pinned, jsonData, secureJsonFields } = plugin.meta;
const [state, setState] = useState<State>({
apiUrl: jsonData?.apiUrl || '',
apiKey: '',
isApiKeySet: Boolean(secureJsonFields?.apiKey),
});
const isSubmitDisabled = Boolean(!state.apiUrl || (!state.isApiKeySet && !state.apiKey));
const onResetApiKey = () =>
setState({
...state,
apiKey: '',
isApiKeySet: false,
});
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setState({
...state,
[event.target.name]: event.target.value.trim(),
});
};
const onSubmit = () => {
if (isSubmitDisabled) {
return;
}
updatePluginAndReload(plugin.meta.id, {
enabled,
pinned,
jsonData: {
apiUrl: state.apiUrl,
},
// This cannot be queried later by the frontend.
// We don't want to override it in case it was set previously and left untouched now.
secureJsonData: state.isApiKeySet
? undefined
: {
apiKey: state.apiKey,
},
});
};
return (
<form onSubmit={onSubmit}>
<FieldSet label="API Settings">
<Field label="API Key" description="A secret key for authenticating to our custom API">
<SecretInput
width={60}
id="config-api-key"
data-testid={testIds.appConfig.apiKey}
name="apiKey"
value={state.apiKey}
isConfigured={state.isApiKeySet}
placeholder={'Your secret API key'}
onChange={onChange}
onReset={onResetApiKey}
/>
</Field>
<Field label="API Url" description="" className={s.marginTop}>
<Input
width={60}
name="apiUrl"
id="config-api-url"
data-testid={testIds.appConfig.apiUrl}
value={state.apiUrl}
placeholder={`E.g.: http://mywebsite.com/api/v1`}
onChange={onChange}
/>
</Field>
<div className={s.marginTop}>
<Button type="submit" data-testid={testIds.appConfig.submit} disabled={isSubmitDisabled}>
Save API settings
</Button>
</div>
</FieldSet>
</form>
);
};
export default AppConfig;
const getStyles = (theme: GrafanaTheme2) => ({
colorWeak: css`
color: ${theme.colors.text.secondary};
`,
marginTop: css`
margin-top: ${theme.spacing(3)};
`,
});
const updatePluginAndReload = async (pluginId: string, data: Partial<PluginMeta<AppPluginSettings>>) => {
try {
await updatePlugin(pluginId, data);
// Reloading the page as the changes made here wouldn't be propagated to the actual plugin otherwise.
// This is not ideal, however unfortunately currently there is no supported way for updating the plugin state.
window.location.reload();
} catch (e) {
console.error('Error while updating the plugin', e);
}
};
const updatePlugin = async (pluginId: string, data: Partial<PluginMeta>) => {
const response = await getBackendSrv().fetch({
url: `/api/plugins/${pluginId}/settings`,
method: 'POST',
data,
});
return lastValueFrom(response);
};

21
enne2corp-fullstacktest-app/src/components/testIds.ts

@ -0,0 +1,21 @@
export const testIds = {
appConfig: {
apiKey: 'data-testid ac-api-key',
apiUrl: 'data-testid ac-api-url',
submit: 'data-testid ac-submit-form',
},
pageOne: {
container: 'data-testid pg-one-container',
navigateToFour: 'data-testid navigate-to-four',
},
pageTwo: {
container: 'data-testid pg-two-container',
},
pageThree: {
container: 'data-testid pg-three-container',
},
pageFour: {
container: 'data-testid pg-four-container',
navigateBack: 'data-testid navigate-back',
},
};

10
enne2corp-fullstacktest-app/src/constants.ts

@ -0,0 +1,10 @@
import pluginJson from './plugin.json';
export const PLUGIN_BASE_URL = `/a/${pluginJson.id}`;
export enum ROUTES {
One = 'one',
Two = 'two',
Three = 'three',
Four = 'four',
}

1
enne2corp-fullstacktest-app/src/img/logo.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 81.9 71.52"><defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:#3865ab;}.cls-3{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="42.95" y1="16.88" x2="81.9" y2="16.88" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M55.46,62.43A2,2,0,0,1,54.07,59l4.72-4.54a2,2,0,0,1,2.2-.39l3.65,1.63,3.68-3.64a2,2,0,1,1,2.81,2.84l-4.64,4.6a2,2,0,0,1-2.22.41L60.6,58.26l-3.76,3.61A2,2,0,0,1,55.46,62.43Z"/><path class="cls-2" d="M37,0H2A2,2,0,0,0,0,2V31.76a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V2A2,2,0,0,0,37,0ZM4,29.76V8.84H35V29.76Z"/><path class="cls-3" d="M79.9,0H45a2,2,0,0,0-2,2V31.76a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V2A2,2,0,0,0,79.9,0ZM47,29.76V8.84h31V29.76Z"/><path class="cls-2" d="M37,37.76H2a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2H37a2,2,0,0,0,2-2V39.76A2,2,0,0,0,37,37.76ZM4,67.52V46.6H35V67.52Z"/><path class="cls-2" d="M79.9,37.76H45a2,2,0,0,0-2,2V69.52a2,2,0,0,0,2,2h35a2,2,0,0,0,2-2V39.76A2,2,0,0,0,79.9,37.76ZM47,67.52V46.6h31V67.52Z"/><rect class="cls-1" x="10.48" y="56.95" width="4" height="5.79"/><rect class="cls-1" x="17.43" y="53.95" width="4" height="8.79"/><rect class="cls-1" x="24.47" y="50.95" width="4" height="11.79"/><path class="cls-1" d="M19.47,25.8a6.93,6.93,0,1,1,6.93-6.92A6.93,6.93,0,0,1,19.47,25.8Zm0-9.85a2.93,2.93,0,1,0,2.93,2.93A2.93,2.93,0,0,0,19.47,16Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

26
enne2corp-fullstacktest-app/src/module.tsx

@ -0,0 +1,26 @@
import React, { Suspense, lazy } from 'react';
import { AppPlugin, type AppRootProps } from '@grafana/data';
import { LoadingPlaceholder } from '@grafana/ui';
import type { AppConfigProps } from './components/AppConfig/AppConfig';
const LazyApp = lazy(() => import('./components/App/App'));
const LazyAppConfig = lazy(() => import('./components/AppConfig/AppConfig'));
const App = (props: AppRootProps) => (
<Suspense fallback={<LoadingPlaceholder text="" />}>
<LazyApp {...props} />
</Suspense>
);
const AppConfig = (props: AppConfigProps) => (
<Suspense fallback={<LoadingPlaceholder text="" />}>
<LazyAppConfig {...props} />
</Suspense>
);
export const plugin = new AppPlugin<{}>().setRootPage(App).addConfigPage({
title: 'Configuration',
icon: 'cog',
body: AppConfig,
id: 'configuration',
});

44
enne2corp-fullstacktest-app/src/pages/PageFour.tsx

@ -0,0 +1,44 @@
import React from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { ROUTES } from '../constants';
import { prefixRoute } from '../utils/utils.routing';
import { testIds } from '../components/testIds';
import { PluginPage } from '@grafana/runtime';
function PageFour() {
const s = useStyles2(getStyles);
return (
<PluginPage layout={PageLayoutType.Canvas}>
<div className={s.page} data-testid={testIds.pageFour.container}>
<div className={s.container}>
<LinkButton data-testid={testIds.pageFour.navigateBack} icon="arrow-left" href={prefixRoute(ROUTES.One)}>
Back
</LinkButton>
<div className={s.content}>This is a full-width page without a navigation bar.</div>
</div>
</div>
</PluginPage>
);
}
export default PageFour;
const getStyles = (theme: GrafanaTheme2) => ({
page: css`
padding: ${theme.spacing(3)};
background-color: ${theme.colors.background.secondary};
display: flex;
justify-content: center;
`,
container: css`
width: 900px;
max-width: 100%;
min-height: 500px;
`,
content: css`
margin-top: ${theme.spacing(6)};
`,
});

165
enne2corp-fullstacktest-app/src/pages/PageOne.tsx

@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles2, Input, Button, Alert } from '@grafana/ui';
import { prefixRoute } from '../utils/utils.routing';
import { ROUTES } from '../constants';
import { testIds } from '../components/testIds';
import { PluginPage, getBackendSrv } from '@grafana/runtime';
function PageOne() {
const s = useStyles2(getStyles);
const [textValue, setTextValue] = useState('');
const [savedValue, setSavedValue] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
// Load saved value when component mounts
useEffect(() => {
loadSavedValue();
}, []);
const loadSavedValue = async () => {
try {
const response = await getBackendSrv().get('/api/plugins/enne2corp-fullstacktest-app/resources/get-text');
setSavedValue(response.message || '');
} catch (err) {
console.log('No saved value found or error loading:', err);
setSavedValue('');
}
};
const handleSaveText = async () => {
if (!textValue.trim()) {
setError('Please enter some text');
return;
}
setLoading(true);
setError('');
setSuccess('');
try {
await getBackendSrv().post('/api/plugins/enne2corp-fullstacktest-app/resources/save-text', {
message: textValue,
});
setSuccess('Text saved successfully!');
setSavedValue(textValue);
setTextValue('');
// Clear success message after 3 seconds
setTimeout(() => setSuccess(''), 3000);
} catch (err) {
setError('Failed to save text. Please try again.');
console.error('Error saving text:', err);
} finally {
setLoading(false);
}
};
return (
<PluginPage>
<div data-testid={testIds.pageOne.container}>
<h2>Welcome Message App</h2>
{savedValue && (
<div className={s.welcomeMessage}>
<h3>Welcome! Your saved message is:</h3>
<p className={s.savedText}>{savedValue}</p>
</div>
)}
{!savedValue && (
<div className={s.noMessage}>
<p>No message saved yet. Enter a message below to get started!</p>
</div>
)}
<div className={s.form}>
<h4>Save a new message:</h4>
<Input
placeholder="Enter your message here..."
value={textValue}
onChange={(e) => setTextValue(e.currentTarget.value)}
disabled={loading}
className={s.input}
/>
<Button
onClick={handleSaveText}
disabled={loading || !textValue.trim()}
className={s.button}
>
{loading ? 'Saving...' : 'Save Message'}
</Button>
</div>
{error && (
<Alert title="Error" severity="error" className={s.alert}>
{error}
</Alert>
)}
{success && (
<Alert title="Success" severity="success" className={s.alert}>
{success}
</Alert>
)}
<div className={s.marginTop}>
<LinkButton data-testid={testIds.pageOne.navigateToFour} href={prefixRoute(ROUTES.Four)}>
Full-width page example
</LinkButton>
</div>
</div>
</PluginPage>
);
}
export default PageOne;
const getStyles = (theme: GrafanaTheme2) => ({
marginTop: css`
margin-top: ${theme.spacing(2)};
`,
welcomeMessage: css`
background: ${theme.colors.background.secondary};
padding: ${theme.spacing(2)};
border-radius: ${theme.shape.borderRadius()};
margin-bottom: ${theme.spacing(2)};
border-left: 4px solid ${theme.colors.primary.main};
`,
savedText: css`
font-size: ${theme.typography.h4.fontSize};
font-weight: ${theme.typography.fontWeightMedium};
color: ${theme.colors.primary.text};
margin: 0;
`,
noMessage: css`
background: ${theme.colors.warning.transparent};
padding: ${theme.spacing(2)};
border-radius: ${theme.shape.borderRadius()};
margin-bottom: ${theme.spacing(2)};
border-left: 4px solid ${theme.colors.warning.main};
`,
form: css`
background: ${theme.colors.background.secondary};
padding: ${theme.spacing(2)};
border-radius: ${theme.shape.borderRadius()};
margin-bottom: ${theme.spacing(2)};
`,
input: css`
margin-bottom: ${theme.spacing(2)};
width: 100%;
max-width: 400px;
`,
button: css`
margin-top: ${theme.spacing(1)};
`,
alert: css`
margin-top: ${theme.spacing(2)};
margin-bottom: ${theme.spacing(2)};
`,
});

49
enne2corp-fullstacktest-app/src/pages/PageThree.tsx

@ -0,0 +1,49 @@
import React from 'react';
import { css } from '@emotion/css';
import { useParams, Link } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { prefixRoute } from '../utils/utils.routing';
import { ROUTES } from '../constants';
import { testIds } from '../components/testIds';
import { PluginPage } from '@grafana/runtime';
function PageThree() {
const s = useStyles2(getStyles);
const { id } = useParams<{ id: string }>();
return (
<PluginPage>
<div data-testid={testIds.pageThree.container}>
This is page three.
<br />
<br />
{/* The ID parameter is set */}
{id && (
<>
<strong>ID:</strong> {id}
</>
)}
{/* No ID parameter */}
{!id && (
<>
<strong>No id parameter is set in the URL.</strong> <br />
Try the following link: <br />
<Link className={s.link} to={prefixRoute(`${ROUTES.Three}/123456789`)}>
{prefixRoute(`${ROUTES.Three}/123456789`)}
</Link>
</>
)}
</div>
</PluginPage>
);
}
export default PageThree;
const getStyles = (theme: GrafanaTheme2) => ({
link: css`
color: ${theme.colors.text.link};
text-decoration: underline;
`,
});

15
enne2corp-fullstacktest-app/src/pages/PageTwo.tsx

@ -0,0 +1,15 @@
import React from 'react';
import { testIds } from '../components/testIds';
import { PluginPage } from '@grafana/runtime';
function PageTwo() {
return (
<PluginPage>
<div data-testid={testIds.pageTwo.container}>
<p>This is page two.</p>
</div>
</PluginPage>
);
}
export default PageTwo;

64
enne2corp-fullstacktest-app/src/plugin.json

@ -0,0 +1,64 @@
{
"$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
"type": "app",
"name": "Fullstack-Test",
"id": "enne2corp-fullstacktest-app",
"backend": true,
"executable": "gpx_fullstack_test",
"info": {
"keywords": ["app"],
"description": "",
"author": {
"name": "Enne2 corp"
},
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"screenshots": [],
"version": "%VERSION%",
"updated": "%TODAY%"
},
"includes": [
{
"type": "page",
"name": "Page One",
"path": "/a/%PLUGIN_ID%/one",
"addToNav": true,
"defaultNav": true
},
{
"type": "page",
"name": "Page Two",
"path": "/a/%PLUGIN_ID%/two",
"addToNav": true,
"defaultNav": false
},
{
"type": "page",
"name": "Page Three",
"path": "/a/%PLUGIN_ID%/three",
"addToNav": true,
"defaultNav": false
},
{
"type": "page",
"name": "Page Four",
"path": "/a/%PLUGIN_ID%/four",
"addToNav": true,
"defaultNav": false
},
{
"type": "page",
"icon": "cog",
"name": "Configuration",
"path": "/plugins/%PLUGIN_ID%",
"role": "Admin",
"addToNav": true
}
],
"dependencies": {
"grafanaDependency": ">=10.4.0",
"plugins": []
}
}

6
enne2corp-fullstacktest-app/src/utils/utils.routing.ts

@ -0,0 +1,6 @@
import { PLUGIN_BASE_URL } from '../constants';
// Prefixes the route with the base URL of the plugin
export function prefixRoute(route: string): string {
return `${PLUGIN_BASE_URL}/${route}`;
}

19
enne2corp-fullstacktest-app/tests/appConfig.spec.ts

@ -0,0 +1,19 @@
import { test, expect } from './fixtures';
test('should be possible to save app configuration', async ({ appConfigPage, page }) => {
const saveButton = page.getByRole('button', { name: /Save API settings/i });
// reset the configured secret
await page.getByRole('button', { name: /reset/i }).click();
// enter some valid values
await page.getByRole('textbox', { name: 'API Key' }).fill('secret-api-key');
await page.getByRole('textbox', { name: 'API Url' }).clear();
await page.getByRole('textbox', { name: 'API Url' }).fill('http://www.my-awsome-grafana-app.com/api');
// listen for the server response on the saved form
const saveResponse = appConfigPage.waitForSettingsResponse();
await saveButton.click();
await expect(saveResponse).toBeOK();
});

31
enne2corp-fullstacktest-app/tests/appNavigation.spec.ts

@ -0,0 +1,31 @@
import { test, expect } from './fixtures';
import { ROUTES } from '../src/constants';
test.describe('navigating app', () => {
test('page one should render successfully', async ({ gotoPage, page }) => {
await gotoPage(`/${ROUTES.One}`);
await expect(page.getByText('This is page one.')).toBeVisible();
});
test('page two should render successfully', async ({ gotoPage, page }) => {
await gotoPage(`/${ROUTES.Two}`);
await expect(page.getByText('This is page two.')).toBeVisible();
});
test('page three should support an id parameter', async ({ gotoPage, page }) => {
await gotoPage(`/${ROUTES.Three}/123456`);
await expect(page.getByText('ID: 123456')).toBeVisible();
});
test('page three should render sucessfully', async ({ gotoPage, page }) => {
// wait for page to successfully render
await gotoPage(`/${ROUTES.One}`);
await expect(page.getByText('This is page one.')).toBeVisible();
// navigating to page four with full width layout without sidebar menu
await page.getByText('Full-width page example').click();
// navigate back to page one
await page.getByRole('link', { name: 'Back', exact: true }).click();
});
});

26
enne2corp-fullstacktest-app/tests/fixtures.ts

@ -0,0 +1,26 @@
import { AppConfigPage, AppPage, test as base } from '@grafana/plugin-e2e';
import pluginJson from '../src/plugin.json';
type AppTestFixture = {
appConfigPage: AppConfigPage;
gotoPage: (path?: string) => Promise<AppPage>;
};
export const test = base.extend<AppTestFixture>({
appConfigPage: async ({ gotoAppConfigPage }, use) => {
const configPage = await gotoAppConfigPage({
pluginId: pluginJson.id,
});
await use(configPage);
},
gotoPage: async ({ gotoAppPage }, use) => {
await use((path) =>
gotoAppPage({
path,
pluginId: pluginJson.id,
})
);
},
});
export { expect } from '@grafana/plugin-e2e';

3
enne2corp-fullstacktest-app/tsconfig.json

@ -0,0 +1,3 @@
{
"extends": "./.config/tsconfig.json"
}
Loading…
Cancel
Save