Merge branch 'main' into drew08t/canvas-add-zoom

This commit is contained in:
drew08t
2023-11-01 18:57:02 -07:00
399 changed files with 15963 additions and 14850 deletions
+8 -99
View File
@@ -1349,9 +1349,6 @@ exports[`better eslint`] = {
"public/app/core/components/PasswordField/PasswordField.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/core/components/PermissionList/AddPermission.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/core/components/QueryOperationRow/OperationRowHelp.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
@@ -1380,8 +1377,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
[0, 0, 0, "Styles should be written using objects.", "6"]
],
"public/app/core/components/RolePicker/RolePickerMenu.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
@@ -2769,43 +2765,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/features/canvas/elements/droneFront.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/canvas/elements/droneSide.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/canvas/elements/droneTop.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/features/canvas/elements/ellipse.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/canvas/elements/icon.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/canvas/elements/metricValue.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/features/canvas/elements/server/server.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"]
],
"public/app/features/canvas/elements/text.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/features/canvas/elements/windTurbine.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/canvas/runtime/element.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -2885,6 +2846,10 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/dashboard-scene/scene/setDashboardPanelContext.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -4193,9 +4158,6 @@ exports[`better eslint`] = {
"public/app/features/expressions/guards.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/folders/state/actions.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/geo/editor/GazetteerPathEditor.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
@@ -7008,77 +6970,24 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/plugins/panel/canvas/components/CanvasTooltip.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/plugins/panel/canvas/components/SetBackground.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/plugins/panel/canvas/components/connections/ConnectionAnchors.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"]
],
"public/app/plugins/panel/canvas/components/connections/ConnectionSVG.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/plugins/panel/canvas/editor/connectionEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/canvas/editor/element/APIEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/canvas/editor/element/ConstraintSelectionBox.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"]
],
"public/app/plugins/panel/canvas/editor/element/PlacementEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/canvas/editor/element/QuickPositioning.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/plugins/panel/canvas/editor/element/elementEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"]
],
"public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"]
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/panel/canvas/editor/layer/TreeNavigationEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/plugins/panel/canvas/editor/layer/TreeNodeTitle.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/canvas/editor/layer/layerEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
+158 -144
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -635,7 +635,6 @@ embed.go @grafana/grafana-as-code
/.github/workflows/issue-opened.yml @grafana/grafana-community-support
/.github/workflows/metrics-collector.yml @torkelo
/.github/workflows/milestone.yml @marefr
/.github/workflows/ox-code-coverage.yml @grafana/explore-squad
/.github/workflows/pr-checks.yml @marefr
/.github/workflows/pr-codeql-analysis-go.yml @DanCech
/.github/workflows/pr-codeql-analysis-javascript.yml @DanCech
+1 -2
View File
@@ -8,7 +8,6 @@
"react-router-dom", // we should bump this together with history (see https://github.com/grafana/grafana/issues/76744)
"monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins
"react-hook-form", // due to us exposing these hooks via @grafana/ui form components bumping can break plugins
"react-redux", // react-beautiful-dnd depends on react-redux 7.x, we need to update that one first
],
"includePaths": ["package.json", "packages/**", "public/app/plugins/**"],
"ignorePaths": ["emails/**", "plugins-bundled/**", "**/mocks/**", "packages/grafana-e2e/**"],
@@ -74,7 +73,7 @@
"groupName": "locker",
"matchPackagePrefixes": [
"@locker/"
]
],
"reviewers": ["team:grafana/plugins-platform-frontend"],
},
],
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Set go version
uses: actions/setup-go@v4
with:
go-version: '1.20.10'
go-version: '1.21.3'
- name: Build swagger
run: |
make -C pkg/services/ngalert/api/tooling post.json api.json
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
name: Set go version
uses: actions/setup-go@v4
with:
go-version: '1.20.10'
go-version: '1.21.3'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
@@ -17,6 +17,11 @@ env:
GCP_BUCKET: integration-artifacts # Dev: plugins-community-staging
GCOM_API: https://grafana.com # Dev: https://grafana-dev.com
# These permissions are needed to assume roles from Github's OIDC.
permissions:
contents: read
id-token: write
jobs:
build-and-publish:
name: Build and publish ${{ inputs.plugin_id }}
@@ -31,10 +36,18 @@ jobs:
- name: Verify inputs
run: |
if [ -z ${{ inputs.plugin_id }} ]; then echo "Missing plugin ID"; exit 1; fi
- id: get-secrets
uses: grafana/shared-workflows/actions/get-vault-secrets@main
with:
# Secrets placed in the ci/repo/grafana/<repo>/<path> path in Vault
repo_secrets: |
PLUGINS_GOOGLE_CREDENTIALS=core-plugins-build-and-release:PLUGINS_GOOGLE_CREDENTIALS
PLUGINS_GRAFANA_API_KEY=core-plugins-build-and-release:PLUGINS_GRAFANA_API_KEY
PLUGINS_GCOM_TOKEN=core-plugins-build-and-release:PLUGINS_GCOM_TOKEN
- name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
credentials_json: '${{ secrets.PLUGINS_GOOGLE_CREDENTIALS }}'
credentials_json: '${{ env.PLUGINS_GOOGLE_CREDENTIALS }}'
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
- name: Setup nodejs environment
@@ -140,11 +153,11 @@ jobs:
mkdir -p ci/jobs/package
bin/grabpl plugin package
env:
GRAFANA_API_KEY: ${{ secrets.PLUGINS_GRAFANA_API_KEY }}
GRAFANA_API_KEY: ${{ env.PLUGINS_GRAFANA_API_KEY }}
PLUGIN_SIGNATURE_TYPE: grafana
- name: Check existing release
env:
GCOM_TOKEN: ${{ secrets.PLUGINS_GCOM_TOKEN }}
GCOM_TOKEN: ${{ env.PLUGINS_GCOM_TOKEN }}
VERSION: ${{ steps.build_frontend.outputs.version }}
run: |
api_res=$(curl -X 'GET' -H "Authorization: Bearer $GCOM_TOKEN" \
@@ -177,7 +190,7 @@ jobs:
- name: Publish new plugin version on grafana.com
working-directory: ${{ steps.get_dir.outputs.dir }}
env:
GCOM_TOKEN: ${{ secrets.PLUGINS_GCOM_TOKEN }}
GCOM_TOKEN: ${{ env.PLUGINS_GCOM_TOKEN }}
VERSION: ${{ steps.build_frontend.outputs.version }}
run: |
echo "Publish new plugin version on grafana.com:"
@@ -1,9 +1,9 @@
# Only runs if anything under the packages/ directory changes.
# Only runs if anything under the packages/ directory changes.
# (Otherwise detect-breaking-changes-build-skip.yml takes over)
name: Levitate / Detect breaking changes
on:
on:
pull_request:
paths:
- 'packages/**'
@@ -24,7 +24,7 @@ jobs:
path: './pr'
- uses: actions/setup-node@v4
with:
node-version: 16.16.0
node-version: 20.9.0
- name: Get yarn cache directory path
id: yarn-cache-dir-path
@@ -72,7 +72,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 16.16.0
node-version: 20.9.0
- name: Get yarn cache directory path
id: yarn-cache-dir-path
-21
View File
@@ -1,21 +0,0 @@
name: Observability Experience test code coverage
on:
pull_request:
paths:
- 'pkg/services/queryhistory/**'
- 'pkg/tsdb/loki/**'
- 'pkg/tsdb/elasticsearch/**'
- 'public/app/features/explore/**'
- 'public/app/features/correlations/**'
- 'public/app/plugins/datasource/loki/**'
- 'public/app/plugins/datasource/elasticsearch/**'
branches-ignore:
- dependabot/**
- backport-*
jobs:
workflow-call:
uses: grafana/code-coverage/.github/workflows/code-coverage.yml@v0.1.20
with:
frontend-path-regexp: public\/app\/features\/(explore|correlations)|public\/app\/plugins\/datasource\/(loki|elasticsearch)
backend-path-regexp: pkg\/services\/(queryhistory)|pkg\/tsdb\/(loki|elasticsearch)
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- name: Set go version
uses: actions/setup-go@v4
with:
go-version: '1.20.10'
go-version: '1.21.3'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: "Setup Go"
uses: "actions/setup-go@v4"
with:
go-version: '1.20.10'
go-version: '1.21.3'
- name: "Verify kinds"
run: go run .github/workflows/scripts/kinds/verify-kinds.go
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
- name: "Setup Go"
uses: "actions/setup-go@v4"
with:
go-version: '1.20.10'
go-version: '1.21.3'
- name: "Verify kinds"
run: go run .github/workflows/scripts/kinds/verify-kinds.go
+9 -4
View File
@@ -12,16 +12,21 @@ jobs:
- name: Update procedure
run: |
BRANCH=update-make-docs
git checkout -b "${BRANCH}"
curl -s -Lo docs/docs.mk https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/docs.mk
curl -s -Lo docs/make-docs https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs
if git diff --exit-code; then exit 0; fi
BRANCH="$(date +%Y-%m-%d)/update-make-docs"
git checkout -b "${BRANCH}"
git add .
git config --local user.email "bot@grafana.com"
git config --local user.name "grafanabot"
git commit -m "Update \`make docs\` procedure"
git commit --message "Update \`make docs\` procedure"
git push -v origin "refs/heads/${BRANCH}"
gh pr create --fill --label no-changelog --label --no-backport --label type/docs
gh pr create --fill \
--label 'backport v10.0.x' \
--label 'backport v10.1.x' \
--label 'backport v10.2.x' \
--label no-changelog \
--label type/docs || true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
- name: "Setup Go"
uses: "actions/setup-go@v4"
with:
go-version: '1.20.10'
go-version: '1.21.3'
- name: "Verify kinds"
run: go run .github/workflows/scripts/kinds/verify-kinds.go
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-874
View File
File diff suppressed because one or more lines are too long
+893
View File
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -1,3 +1,7 @@
compressionLevel: mixed
enableGlobalCache: false
enableTelemetry: false
nodeLinker: pnp
@@ -28,14 +32,10 @@ packageExtensions:
react-dom: 17.0.1
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: '@yarnpkg/plugin-typescript'
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: '@yarnpkg/plugin-interactive-tools'
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: 'https://mskelton.dev/yarn-outdated/v2'
yarnPath: .yarn/releases/yarn-3.6.4.cjs
yarnPath: .yarn/releases/yarn-4.0.0.cjs
# Uncomment the following lines if you want to use Verdaccio local npm registry. Read more at packages/README.md
# npmScopes:
# grafana:
+1 -1
View File
@@ -3,7 +3,7 @@
ARG BASE_IMAGE=alpine:3.18.3
ARG JS_IMAGE=node:20-alpine3.18
ARG JS_PLATFORM=linux/amd64
ARG GO_IMAGE=golang:1.20.10-alpine3.18
ARG GO_IMAGE=golang:1.21.3-alpine3.18
ARG GO_SRC=go-builder
ARG JS_SRC=js-builder
+1 -1
View File
@@ -261,7 +261,7 @@ build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development.
--build-arg COMMIT_SHA=$$(git rev-parse HEAD) \
--build-arg BUILD_BRANCH=$$(git rev-parse --abbrev-ref HEAD) \
--build-arg BASE_IMAGE=ubuntu:22.04 \
--build-arg GO_IMAGE=golang:1.20.10 \
--build-arg GO_IMAGE=golang:1.21.3 \
--tag grafana/grafana$(TAG_SUFFIX):dev-ubuntu \
$(DOCKER_BUILD_ARGS)
+31
View File
@@ -0,0 +1,31 @@
# Deprecation policy
We do our best to limit breaking changes and the deprecation of features to major releases. We always do our best **not** to introduce breaking changes in order to make upgrading Grafana as easy and reliable as possible. However, at times we have to introduce a breaking change by changing behavior or by removing a feature.
To minimize the negative effects of removing a feature we require a deprecation plan that includes:
- Determine usage levels of the feature.
- Find alternative solutions and possible migration paths.
- Announce deprecation of the feature.
- Migrate users if possible
- Give users time to adjust to the deprecation.
- Disable the feature by default.
- Remove the feature from the code base.
Depending on the size and importance of the feature this can be a design doc or an issue. We want this to be written communication for all parties so we know it's intentional and that did a reasonable attempt to avoid breaking changes unless needed. The size of the feature also means different notice times between Depreciation and disabling as well as disabling and removal. The actual duration will depend on releases of Grafana and the table below should be used as a guide.
Grafana employees can find more details in our internal docs.
## Grace period between announcement and disabling feature by default
| Size | Duration | Example |
| ------ | ---------- | ---------------------------------------------------------------- |
| Large | 1-2 years | Classic alerting, scripted dashboards, AngularJS |
| Medium | 6 months | Supported Database for Grafana's backend |
| Small | 1-3 months | Refresh OAuth access_token automatically using the refresh_token |
## Announced deprecations.
| Name | Annoucement Date | Disabling date | Removal Date | Description | Status |
| ------------------------------------------------------------------------ | ---------------- | -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | ------- |
| [Support for Mysql 5.7](https://github.com/grafana/grafana/issues/68446) | 2023-05-15 | October 2023 | | MySQL 5.7 is being deprecated in October 2023 and Grafana's policy is to test against the officially supported version. | Planned |
+2 -3
View File
@@ -10,7 +10,6 @@ include docs.mk
.PHONY: sources/panels-visualizations/query-transform-data/transform-data/index.md
sources/panels-visualizations/query-transform-data/transform-data/index.md: ## Generate the Transform Data page source.
sources/panels-visualizations/query-transform-data/transform-data/index.md:
cd $(CURDIR)/..
npx tsc ./scripts/docs/generate-transformations.ts
node ./scripts/docs/generate-transformations.js > $(CURDIR)/$@
cd $(CURDIR)/.. && npx tsc ./scripts/docs/generate-transformations.ts && \
node ./scripts/docs/generate-transformations.js > $(CURDIR)/$@ && \
npx prettier -w $(CURDIR)/$@
@@ -59,13 +59,14 @@ If you have a literal `$` in your value and want to avoid interpolation, `$$` ca
Currently we do not provide any scripts/manifests for configuring Grafana. Rather than spending time learning and creating scripts/manifests for each tool, we think our time is better spent making Grafana easier to provision. Therefore, we heavily rely on the expertise of the community.
| Tool | Project |
| --------- | -------------------------------------------------------------------------------------------------------------- |
| Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana) |
| Ansible | [https://github.com/grafana/grafana-ansible-collection](https://github.com/grafana/grafana-ansible-collection) |
| Chef | [https://github.com/sous-chefs/chef-grafana](https://github.com/sous-chefs/chef-grafana) |
| Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana) |
| Jsonnet | [https://github.com/grafana/grafonnet-lib/](https://github.com/grafana/grafonnet-lib/) |
| Tool | Project |
| --------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana) |
| Ansible | [https://github.com/grafana/grafana-ansible-collection](https://github.com/grafana/grafana-ansible-collection) |
| Chef | [https://github.com/sous-chefs/chef-grafana](https://github.com/sous-chefs/chef-grafana) |
| Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana) |
| Jsonnet | [https://github.com/grafana/grafonnet-lib/](https://github.com/grafana/grafonnet-lib/) |
| NixOS | [services.grafana.provision module](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/monitoring/grafana.nix) |
## Data sources
+48 -1
View File
@@ -29,13 +29,59 @@ Starting from version 9.1, there is also a [OpenAPI v3 specification](https://ed
Users can browser and try out both via the Swagger UI editor (served by the grafana server) by navigating to `/swagger-ui` and `/openapi3` respectively.
## Authenticating API requests
You can authenticate requests using basic auth, a service account token or a session cookie (acquired using regular login or OAuth).
### Basic Auth
If basic auth is enabled (it is enabled by default), then you can authenticate your HTTP request via
standard basic auth. Basic auth will also authenticate LDAP users.
curl example:
```bash
curl http://admin:admin@localhost:3000/api/org
{"id":1,"name":"Main Org."}
```
### Service Account Token
To create a service account token, click on **Administration** in the left-side menu, and then **Service Accounts**.
For more information on how to use service account tokens, refer to the [Service Accounts]({{< relref "../../administration/service-accounts/" >}}) documentation.
You use the token in all requests in the `Authorization` header, like this:
**Example**:
```http
GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
The `Authorization` header value should be `Bearer <your service account token>`.
## X-Grafana-Org-Id Header
**X-Grafana-Org-Id** is an optional property that specifies the organization to which the action is applied. If it is not set, the created key belongs to the current context org. Use this header in all requests except those regarding admin.
**Example Request**:
```http
GET /api/org/ HTTP/1.1
Accept: application/json
Content-Type: application/json
X-Grafana-Org-Id: 2
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
## HTTP APIs
- [Admin API]({{< relref "admin/" >}})
- [Alerting API (unstable)](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json)
- [Alerting Provisioning API]({{< relref "alerting_provisioning/" >}})
- [Annotations API]({{< relref "annotations/" >}})
- [Authentication API]({{< relref "auth/" >}})
- [Correlations API]({{< relref "correlations/" >}})
- [Dashboard API]({{< relref "dashboard/" >}})
- [Dashboard Permissions API]({{< relref "dashboard_permissions/" >}})
@@ -59,6 +105,7 @@ Users can browser and try out both via the Swagger UI editor (served by the graf
- [Alerting Notification Channels API]({{< relref "alerting_notification_channels/" >}})
- [Alerting API]({{< relref "alerting/" >}})
- [Authentication API]({{< relref "auth/" >}})
## Grafana Enterprise HTTP APIs
+7 -64
View File
@@ -19,72 +19,15 @@ title: 'Authentication HTTP API '
# Authentication API
The Authentication HTTP API is used to manage API keys.
{{% admonition type="note" %}}
If you use Grafana v9.1 or newer, use service accounts instead of API keys. For more information, refer to [Grafana service account API reference]({{< relref "./serviceaccount/" >}}).
{{% /admonition %}}
> If you are running Grafana Enterprise, for some endpoints you would need to have relevant permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
## Tokens
Currently, you can authenticate via an `API Token` or via a `Session cookie` (acquired using regular login or OAuth).
## X-Grafana-Org-Id Header
**X-Grafana-Org-Id** is an optional property that specifies the organization to which the action is applied. If it is not set, the created key belongs to the current context org. Use this header in all requests except those regarding admin.
**Example Request**:
```http
POST /api/auth/keys HTTP/1.1
Accept: application/json
Content-Type: application/json
X-Grafana-Org-Id: 2
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name": "mykey",
"role": "Admin",
"secondsToLive": 86400
}
```
## Basic Auth
If basic auth is enabled (it is enabled by default), then you can authenticate your HTTP request via
standard basic auth. Basic auth will also authenticate LDAP users.
curl example:
```bash
curl http://admin:admin@localhost:3000/api/org
{"id":1,"name":"Main Org."}
```
## Create API Token
Open the sidemenu and click the organization dropdown and select the `API Keys` option.
You use the token in all requests in the `Authorization` header, like this:
**Example**:
```http
GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
The `Authorization` header value should be `Bearer <your api key>`.
The API Token can also be passed as a Basic authorization password with the special username `api_key`:
curl example:
```bash
curl http://api_key:eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk@localhost:3000/api/org
{"id":1,"name":"Main Org."}
```
# Auth HTTP resources / actions
## Api Keys
## List API keys
`GET /api/auth/keys`
@@ -14,11 +14,11 @@ labels:
products:
- enterprise
- oss
title: 'API Tutorial: Create API tokens and dashboards for an organization'
title: 'API Tutorial: Create Service Account tokens and dashboards for an organization'
weight: 150
---
# Create API tokens and dashboards for an organization
# Create Service Account tokens and dashboards for an organization
Use the Grafana API to set up new Grafana organizations or to add dynamically generated dashboards to an existing organization.
@@ -27,9 +27,9 @@ Use the Grafana API to set up new Grafana organizations or to add dynamically ge
There are two authentication methods to access the API:
- Basic authentication: A Grafana Admin user can access some parts of the Grafana API through basic authentication.
- API Tokens: All organization actions are accessed through an API Token. An API Token is associated with an organization. It can be used to create dashboards and other components specific for that organization.
- Service Account tokens: All organization actions can be accessed through a Service Account token. A Service Account token is associated with an organization. It can be used to create dashboards and other components specific for that organization.
## How to create a new organization and an API Token
## How to create a new organization and a Service Account Token
The task is to create a new organization and then add a Token that can be used by other users. In the examples below which use basic auth, the user is `admin` and the password is `admin`.
@@ -53,13 +53,30 @@ The task is to create a new organization and then add a Token that can be used b
curl -X POST http://admin:admin@localhost:3000/api/user/using/<id of new org>
```
1. [Create the API token](http://docs.grafana.org/http_api/auth/#create-api-key):
1. [Create a Service Account]({{< relref "./serviceaccount/#create-service-account" >}}):
```bash
curl -X POST -H "Content-Type: application/json" -d '{"name":"apikeycurl", "role": "Admin"}' http://admin:admin@localhost:3000/api/auth/keys
curl -X POST -H "Content-Type: application/json" -d '{"name":"test", "role": "Admin"}' http://admin:admin@localhost:3000/api/serviceaccounts
```
This should return a response: `{"name":"apikeycurl","key":"eyJrIjoiR0ZXZmt1UFc0OEpIOGN5RWdUalBJTllUTk83VlhtVGwiLCJuIjoiYXBpa2V5Y3VybCIsImlkIjo2fQ=="}`.
1. [Create a Service Account token]({{< relref "./serviceaccount/#create-service-account-tokens" >}}) for the service account created in the previous step:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"name":"test-token"}' http://admin:admin@localhost:3000/api/serviceaccounts/<service account id>/tokens
```
This should return a response:
```http
HTTP/1.1 200
Content-Type: application/json
{
"id": 7,
"name": "test-token",
"key": "eyJrIjoiVjFxTHZ6dGdPSjg5Um92MjN1RlhjMkNqYkZUbm9jYkwiLCJuIjoiZ3JhZmFuYSIsImlkIjoxfQ=="
}
```
Save the key returned here in your password manager as it is not possible to fetch again it in the future.
@@ -28,5 +28,6 @@ title: GrafanaPyroscopeDataQuery kind
| `hide` | boolean | No | | true if query is disabled (ie should not be returned to the dashboard)<br/>Note this does not always imply that the query should not be executed since<br/>the results from a hidden query may be used as the input to other queries (SSE etc) |
| `maxNodes` | integer | No | | Sets the maximum number of nodes in the flamegraph. |
| `queryType` | string | No | | Specify the query flavor<br/>TODO make this required and give it a default |
| `spanSelector` | string[] | No | | Specifies the query span selectors. |
@@ -29,9 +29,9 @@ The following table contains a list of calculations you can perform in Grafana.
| Difference percent | Percentage change between first and last value of a field |
| Distinct count | Number of unique values in a field |
| First | First value in a field |
| First\* (not null) | First, not null value in a field |
| First\* (not null) | First, not null value in a field (also excludes NaNs) |
| Last | Last value in a field |
| Last\* (not null) | Last, not null value in a field |
| Last\* (not null) | Last, not null value in a field (also excludes NaNs) |
| Max | Maximum value of a field |
| Mean | Mean value of all values in a field |
| Variance | Variance of all values in a field |
@@ -132,6 +132,7 @@ Experimental features might be changed or removed without prior notice.
| `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources |
| `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server |
| `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end |
| `traceToProfiles` | Enables linking between traces and profiles |
| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder |
| `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI |
| `angularDeprecationUI` | Display new Angular deprecation-related UI features |
@@ -147,6 +148,7 @@ Experimental features might be changed or removed without prior notice.
| `formatString` | Enable format string transformer |
| `transformationsVariableSupport` | Allows using variables in transformations |
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists |
| `kubernetesPlaylistsAPI` | Route /api/playlist API to k8s handlers |
| `navAdminSubsections` | Splits the administration section of the nav tree into subsections |
| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression |
| `teamHttpHeaders` | Enables datasources to apply team headers to the client requests |
@@ -159,6 +161,7 @@ Experimental features might be changed or removed without prior notice.
| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. |
| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. |
| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. |
| `annotationPermissionUpdate` | Separate annotation permissions from dashboard permissions to allow for more granular control. |
## Development feature toggles
@@ -3,7 +3,8 @@ import { e2e } from '../utils';
const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b';
const DATAGRID_SELECT_SERIES = 'Datagrid Select series';
describe('Datagrid data changes', () => {
//TODO enable this test when panel goes live
describe.skip('Datagrid data changes', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
@@ -3,7 +3,8 @@ import { e2e } from '../utils';
const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b';
const DATAGRID_CANVAS = 'data-grid-canvas';
describe('Datagrid data changes', () => {
//TODO enable this test when panel goes live
describe.skip('Datagrid data changes', () => {
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
});
+12 -12
View File
@@ -1,6 +1,6 @@
module github.com/grafana/grafana
go 1.20
go 1.21
// Override docker/docker to avoid:
// go: github.com/drone-runners/drone-runner-docker@v1.8.2 requires
@@ -61,7 +61,7 @@ require (
github.com/google/uuid v1.3.1 // @grafana/backend-platform
github.com/google/wire v0.5.0 // @grafana/backend-platform
github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20231026192550-079966731bbe // @grafana/alerting-squad-backend
github.com/grafana/alerting v0.0.0-20231101090315-bf12694896a8 // @grafana/alerting-squad-backend
github.com/grafana/cuetsy v0.1.10 // @grafana/grafana-as-code
github.com/grafana/grafana-aws-sdk v0.19.1 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go v1.9.0 // @grafana/backend-platform
@@ -88,8 +88,8 @@ require (
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/alertmanager v0.25.0 // @grafana/alerting-squad-backend
github.com/prometheus/client_golang v1.16.0 // @grafana/alerting-squad-backend
github.com/prometheus/client_model v0.4.0 // @grafana/backend-platform
github.com/prometheus/client_golang v1.17.0 // @grafana/alerting-squad-backend
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // @grafana/backend-platform
github.com/prometheus/common v0.44.0 // @grafana/alerting-squad-backend
github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 // @grafana/alerting-squad-backend
github.com/robfig/cron/v3 v3.0.1 // @grafana/backend-platform
@@ -113,7 +113,7 @@ require (
golang.org/x/oauth2 v0.13.0 // @grafana/grafana-authnz-team
golang.org/x/sync v0.4.0 // @grafana/alerting-squad-backend
golang.org/x/time v0.3.0 // @grafana/backend-platform
golang.org/x/tools v0.12.0 // @grafana/grafana-as-code
golang.org/x/tools v0.13.0 // @grafana/grafana-as-code
gonum.org/v1/gonum v0.12.0 // @grafana/observability-metrics
google.golang.org/api v0.148.0 // @grafana/backend-platform
google.golang.org/grpc v1.58.3 // @grafana/plugins-platform-backend
@@ -164,7 +164,7 @@ require (
github.com/go-openapi/validate v0.22.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // @grafana/backend-platform
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang/glog v1.1.0 // indirect
github.com/golang/glog v1.1.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // @grafana/backend-platform
github.com/google/btree v1.1.2 // indirect
@@ -199,9 +199,9 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common/sigv4 v0.1.0 // indirect
github.com/prometheus/exporter-toolkit v0.10.0 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b // indirect
github.com/rs/cors v1.9.0 // indirect
github.com/rs/cors v1.10.1 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/segmentio/encoding v0.3.6 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
@@ -286,7 +286,7 @@ require (
require github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad
require github.com/grafana/pyroscope/api v0.2.0 // @grafana/observability-traces-and-profiling
require github.com/grafana/pyroscope/api v0.2.1 // @grafana/observability-traces-and-profiling
require github.com/apache/arrow/go/v13 v13.0.0 // @grafana/observability-metrics
@@ -316,7 +316,7 @@ require (
github.com/cristalhq/jwt/v4 v4.0.2 // indirect
github.com/dave/jennifer v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
@@ -425,7 +425,7 @@ require (
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // @grafana-app-platform-squad
sigs.k8s.io/yaml v1.3.0 // indirect; @grafana-app-platform-squad
)
require (
@@ -500,6 +500,6 @@ replace xorm.io/xorm => github.com/grafana/xorm v0.8.3-0.20230627081928-d04aa38a
// Use our fork of the upstream alertmanagers.
// This is required in order to get notification delivery errors from the receivers API.
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7
replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
+51 -19
View File
@@ -837,7 +837,9 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ=
github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8=
github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk=
github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
@@ -902,6 +904,7 @@ github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230112175826-46e39c7b9b43/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E=
github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw=
@@ -992,6 +995,8 @@ github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXh
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
@@ -1059,6 +1064,7 @@ github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027 h1:1L0aalTpPz7YlMx
github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/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/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
@@ -1080,10 +1086,12 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI=
github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM=
github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@@ -1191,6 +1199,7 @@ github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A=
github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
@@ -1312,7 +1321,9 @@ github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
@@ -1620,6 +1631,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw=
github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -1664,6 +1677,7 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 h1:utua3L2IbQJmauC5IXdEA547bcoU5dozgQAfc8Onsg4=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -1799,10 +1813,10 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/grafana/alerting v0.0.0-20231017091417-a53b5db2235d h1:fxHDUyKFc1mfyJAtW+Qxi66dMahYu+o5/lH+f8SoMHg=
github.com/grafana/alerting v0.0.0-20231017091417-a53b5db2235d/go.mod h1:6BES5CyEqz7fDAG3MYvJLe0hqGwvIoGDN8A1aNrLGus=
github.com/grafana/alerting v0.0.0-20231026192550-079966731bbe h1:6jY5mWR//GbYOjvqpnoncgkdxbeYImkTsy9rPvVFOlk=
github.com/grafana/alerting v0.0.0-20231026192550-079966731bbe/go.mod h1:6BES5CyEqz7fDAG3MYvJLe0hqGwvIoGDN8A1aNrLGus=
github.com/grafana/alerting v0.0.0-20231101090315-bf12694896a8 h1:uoaAgS743libLwhuQUQEDO2YpFaQI5viyY4NrcyibUg=
github.com/grafana/alerting v0.0.0-20231101090315-bf12694896a8/go.mod h1:lR9bhQrESIeOqKtC4Y+fK4mqtLmJFDffFt9q4cWRa8k=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=
@@ -1829,10 +1843,12 @@ github.com/grafana/grafana-plugin-sdk-go v0.187.0 h1:lOwoFbbTs27KqR3F32GvOX9Et3E
github.com/grafana/grafana-plugin-sdk-go v0.187.0/go.mod h1:PHK8eQOz3ES28RmImdTHNOTxBZaH6mb/ytJGxk7VVJc=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7 h1:7gsywzIb39SYZEp9qOnNaxD4d9OOkAfJGvnRUQUtlTM=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7/go.mod h1:0SOsbwZb277OdNdUR+376IxcAYV3Pp+igiAOJdr/98M=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758 h1:ATUhvJSJwzdzhnmzUI92fxVFqyqmcnzJ47wtHTK3LW4=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20231027171310-70c52bf65758/go.mod h1:MmLemcsGjpbOwEeT3k7K+gnvIImXgkatCfVX6sOtx80=
github.com/grafana/pyroscope/api v0.2.0 h1:TzOxL0s6SiaLEy944ZAKgHcx/JDRJXu4O8ObwkqR6p4=
github.com/grafana/pyroscope/api v0.2.0/go.mod h1:nhH+xai9cYFgs6lMy/+L0pKj0d5yCMwji/QAiQFCP+U=
github.com/grafana/pyroscope/api v0.2.1 h1:V/GSrwSN5HgA4Ijf/2SN9Sib55E/xObswaCMkdOOsxs=
github.com/grafana/pyroscope/api v0.2.1/go.mod h1:vNO/Rym3pwNIN4y/f0ACrk5iR7DdWlsdfZGSZE+XChU=
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
@@ -2342,6 +2358,7 @@ github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0Gq
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
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=
@@ -2399,6 +2416,7 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w=
github.com/oleiade/reflections v1.0.1 h1:D1XO3LVEYroYskEsoSiGItp9RUxG6jWnCVvrqH0HHQM=
github.com/oleiade/reflections v1.0.1/go.mod h1:rdFxbxq4QXVZWj0F+e9jqjDkc7dbp97vkRixKo2JR60=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
@@ -2412,12 +2430,14 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@@ -2434,6 +2454,7 @@ github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8lu
github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -2558,8 +2579,8 @@ github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrb
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -2567,8 +2588,9 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@@ -2608,9 +2630,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/prometheus/prometheus v0.43.0 h1:18iCSfrbAHbXvYFvR38U1Pt4uZmU9SmDcCpCrBKUiGg=
github.com/prometheus/prometheus v0.43.0/go.mod h1:2BA14LgBeqlPuzObSEbh+Y+JwLH2GcqDlJKbF2sA6FM=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
@@ -2649,8 +2670,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
@@ -2824,12 +2845,14 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y=
github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f h1:A+MmlgpvrHLeUP8dkBVn4Pnf5Bp5Yk2OALm7SEJLLE8=
github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f/go.mod h1:OBcG9bn7sHtXgarhUEb3OfCnNsgtGnkVf41ilSZ3K3E=
@@ -2849,6 +2872,7 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
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=
@@ -2945,6 +2969,7 @@ go.elastic.co/fastjson v1.0.0/go.mod h1:PmeUOMMtLHQr9ZS9J9owrAVg0FkaZDRZJEFTTGHt
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@@ -2958,13 +2983,17 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.9/go.mod h1:y+CzeSmkMpWN2Jyu1npecjB9BBnABxGM4
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU=
go.etcd.io/etcd/client/v2 v2.305.7 h1:AELPkjNR3/igjbO7CjyF1fPuVPjrblliiKj+Y6xSGOU=
go.etcd.io/etcd/client/v2 v2.305.7/go.mod h1:GQGT5Z3TBuAQGvgPfhR7VPySu/SudxmEkRq9BgzFU6s=
go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.etcd.io/etcd/client/v3 v3.5.9 h1:r5xghnU7CwbUxD/fbUtRyJGaYNfDun8sp/gTr1hew6E=
go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA=
go.etcd.io/etcd/pkg/v3 v3.5.7 h1:obOzeVwerFwZ9trMWapU/VjDcYUJb5OfgC1zqEGWO/0=
go.etcd.io/etcd/pkg/v3 v3.5.7/go.mod h1:kcOfWt3Ov9zgYdOiJ/o1Y9zFfLhQjylTgL4Lru8opRo=
go.etcd.io/etcd/raft/v3 v3.5.7 h1:aN79qxLmV3SvIq84aNTliYGmjwsW6NqJSnqmI1HLJKc=
go.etcd.io/etcd/raft/v3 v3.5.7/go.mod h1:TflkAb/8Uy6JFBxcRaH2Fr6Slm9mCPVdI2efzxY96yU=
go.etcd.io/etcd/server/v3 v3.5.7 h1:BTBD8IJUV7YFgsczZMHhMTS67XuA4KpRquL0MFOJGRk=
go.etcd.io/etcd/server/v3 v3.5.7/go.mod h1:gxBgT84issUVBRpZ3XkW1T55NjOb4vZZRI4wVvNhf4A=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -3134,7 +3163,7 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -3313,7 +3342,7 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -3370,7 +3399,6 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
@@ -3532,6 +3560,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -3540,7 +3569,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -3555,7 +3586,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -3574,7 +3605,6 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -3714,8 +3744,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -4218,12 +4248,14 @@ modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+6 -8
View File
@@ -82,7 +82,7 @@
"@betterer/cli": "5.4.0",
"@betterer/eslint": "5.4.0",
"@betterer/regexp": "5.4.0",
"@cypress/webpack-preprocessor": "5.17.1",
"@cypress/webpack-preprocessor": "6.0.0",
"@emotion/eslint-plugin": "11.11.0",
"@grafana/eslint-config": "6.0.1",
"@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules",
@@ -96,7 +96,7 @@
"@swc/core": "1.3.38",
"@swc/helpers": "0.4.14",
"@testing-library/dom": "9.3.3",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/jest-dom": "6.1.4",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.5.1",
"@types/angular": "1.8.5",
@@ -124,7 +124,7 @@
"@types/lucene": "^2",
"@types/marked": "5.0.1",
"@types/mousetrap": "1.6.11",
"@types/node": "18.18.4",
"@types/node": "20.8.10",
"@types/node-forge": "^1",
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.0",
"@types/papaparse": "5.3.7",
@@ -135,7 +135,6 @@
"@types/react-dom": "18.2.7",
"@types/react-grid-layout": "1.3.2",
"@types/react-highlight-words": "0.16.4",
"@types/react-redux": "7.1.25",
"@types/react-router-dom": "5.3.3",
"@types/react-table": "7.7.14",
"@types/react-test-renderer": "18.0.0",
@@ -228,7 +227,6 @@
"ts-jest": "29.1.1",
"ts-node": "10.9.1",
"typescript": "4.8.4",
"wait-on": "7.0.1",
"webpack": "5.89.0",
"webpack-bundle-analyzer": "4.9.0",
"webpack-cli": "5.1.4",
@@ -257,7 +255,7 @@
"@grafana/lezer-traceql": "0.0.8",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^1.18.0",
"@grafana/scenes": "^1.20.1",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"@kusto/monaco-kusto": "^7.4.0",
@@ -383,7 +381,7 @@
"react-moveable": "0.46.1",
"react-popper": "2.3.0",
"react-popper-tooltip": "4.4.2",
"react-redux": "7.2.6",
"react-redux": "8.1.3",
"react-resizable": "3.0.5",
"react-responsive-carousel": "^3.2.23",
"react-router-dom": "5.3.3",
@@ -445,7 +443,7 @@
"engines": {
"node": ">= 20"
},
"packageManager": "yarn@3.6.4",
"packageManager": "yarn@4.0.0",
"dependenciesMeta": {
"prettier@3.0.0": {
"unplugged": true
+2 -2
View File
@@ -67,7 +67,7 @@
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@testing-library/dom": "9.3.3",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/jest-dom": "6.1.4",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.5.1",
"@types/dompurify": "^2",
@@ -76,7 +76,7 @@
"@types/jquery": "3.5.16",
"@types/lodash": "4.14.195",
"@types/marked": "5.0.1",
"@types/node": "18.18.4",
"@types/node": "20.8.10",
"@types/papaparse": "5.3.7",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
@@ -54,7 +54,7 @@ describe('Stats Calculators', () => {
it('should calculate basic stats', () => {
const stats = reduceField({
field: basicTable.fields[0],
reducers: ['first', 'last', 'mean', 'count'],
reducers: [ReducerID.first, ReducerID.last, ReducerID.mean, ReducerID.count],
});
expect(stats.first).toEqual(10);
@@ -67,7 +67,7 @@ describe('Stats Calculators', () => {
basicTable.fields[0].state = undefined; // clear the cache
const stats = reduceField({
field: basicTable.fields[0],
reducers: ['first'],
reducers: [ReducerID.first],
});
// Should do the simple version that just looks up value
@@ -172,6 +172,58 @@ describe('Stats Calculators', () => {
}
});
it('consistent results for first/last value with NaN', () => {
const info = [
{
data: [NaN, 200, NaN], // first/last value is NaN
result: 200,
},
{
data: [NaN, NaN, NaN], // All NaN
result: null,
},
{
data: [undefined, undefined, undefined], // Empty row
result: null,
},
];
const stats = reduceField({
field: createField('x', info[0].data),
reducers: [ReducerID.first, ReducerID.last, ReducerID.firstNotNull, ReducerID.lastNotNull, ReducerID.diffperc],
});
expect(stats[ReducerID.first]).toEqual(NaN);
expect(stats[ReducerID.last]).toEqual(NaN);
expect(stats[ReducerID.firstNotNull]).toEqual(200);
expect(stats[ReducerID.lastNotNull]).toEqual(200);
expect(stats[ReducerID.diffperc]).toEqual(0);
const reducers = [ReducerID.lastNotNull, ReducerID.firstNotNull];
for (const input of info) {
for (const reducer of reducers) {
const v1 = reduceField({
field: createField('x', input.data),
reducers: [reducer, ReducerID.mean], // uses standard path
})[reducer];
const v2 = reduceField({
field: createField('x', input.data),
reducers: [reducer], // uses optimized path
})[reducer];
if (v1 !== v2 || v1 !== input.result) {
const msg =
`Invalid ${reducer} result for: ` +
input.data.join(', ') +
` Expected: ${input.result}` + // configured
` Received: Multiple: ${v1}, Single: ${v2}`;
expect(msg).toEqual(null);
}
}
}
});
it('count should ignoreNulls by default', () => {
const someNulls = createField('x', [1, null, null, 1]);
expect(reduce(someNulls, ReducerID.count)).toEqual(2);
@@ -137,7 +137,7 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
{
id: ReducerID.lastNotNull,
name: 'Last *',
description: 'Last non-null value',
description: 'Last non-null value (also excludes NaNs)',
standard: true,
aliasIds: ['current'],
reduce: calculateLastNotNull,
@@ -152,7 +152,7 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
{
id: ReducerID.firstNotNull,
name: 'First *',
description: 'First non-null value',
description: 'First non-null value (also excludes NaNs)',
standard: true,
reduce: calculateFirstNotNull,
},
@@ -320,8 +320,8 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero:
calcs.count++;
if (currentValue != null) {
// null || undefined
if (currentValue != null && !Number.isNaN(currentValue)) {
// null || undefined || NaN
const isFirst = calcs.firstNotNull === null;
if (isFirst) {
calcs.firstNotNull = currentValue;
@@ -418,7 +418,7 @@ function calculateFirstNotNull(field: Field, ignoreNulls: boolean, nullAsZero: b
const data = field.values;
for (let idx = 0; idx < data.length; idx++) {
const v = data[idx];
if (v != null && v !== undefined) {
if (v != null && !Number.isNaN(v)) {
return { firstNotNull: v };
}
}
@@ -435,7 +435,7 @@ function calculateLastNotNull(field: Field, ignoreNulls: boolean, nullAsZero: bo
let idx = data.length - 1;
while (idx >= 0) {
const v = data[idx--];
if (v != null && v !== undefined) {
if (v != null && !Number.isNaN(v)) {
return { lastNotNull: v };
}
}
@@ -112,6 +112,7 @@ export interface FeatureToggles {
awsAsyncQueryCaching?: boolean;
splitScopes?: boolean;
azureMonitorDataplane?: boolean;
traceToProfiles?: boolean;
permissionsFilterRemoveSubquery?: boolean;
prometheusConfigOverhaulAuth?: boolean;
configurableSchedulerTick?: boolean;
@@ -139,6 +140,7 @@ export interface FeatureToggles {
formatString?: boolean;
transformationsVariableSupport?: boolean;
kubernetesPlaylists?: boolean;
kubernetesPlaylistsAPI?: boolean;
cloudWatchBatchQueries?: boolean;
navAdminSubsections?: boolean;
recoveryThreshold?: boolean;
@@ -153,4 +155,5 @@ export interface FeatureToggles {
alertmanagerRemoteSecondary?: boolean;
alertmanagerRemotePrimary?: boolean;
alertmanagerRemoteOnly?: boolean;
annotationPermissionUpdate?: boolean;
}
+3 -3
View File
@@ -140,9 +140,9 @@ export interface DataSourceWithLogsContextSupport<TQuery extends DataQuery = Dat
getLogRowContextQuery?: (row: LogRowModel, options?: LogRowContextOptions, query?: TQuery) => Promise<TQuery | null>;
/**
* This method can be used to show "context" button based on runtime conditions (for example row model data or plugin settings, etc.)
* @deprecated Deprecated since 10.3. To display the context option and support the feature implement DataSourceWithLogsContextSupport interface instead.
*/
showContextToggle(row?: LogRowModel): boolean;
showContextToggle?(row?: LogRowModel): boolean;
/**
* This method can be used to display a custom UI in the context view.
@@ -157,7 +157,7 @@ export const hasLogsContextSupport = (datasource: unknown): datasource is DataSo
return false;
}
return 'getLogRowContext' in datasource && 'showContextToggle' in datasource;
return 'getLogRowContext' in datasource;
};
/**
@@ -350,19 +350,25 @@ describe('to nanoseconds', () => {
it('should correctly display as minutes', () => {
const eightMinutes = toNanoSeconds(480000000000);
expect(eightMinutes.text).toBe('8');
expect(eightMinutes.suffix).toBe(' mins');
});
it('should correctly display as minute', () => {
const eightMinutes = toNanoSeconds(60000000000);
expect(eightMinutes.text).toBe('1');
expect(eightMinutes.suffix).toBe(' min');
});
it('should correctly display as hours', () => {
const nineHours = toNanoSeconds(32400000000000);
expect(nineHours.text).toBe('9');
expect(nineHours.suffix).toBe(' hour');
expect(nineHours.suffix).toBe(' hours');
});
it('should correctly display as days', () => {
const tenDays = toNanoSeconds(864000000000000);
expect(tenDays.text).toBe('10');
expect(tenDays.suffix).toBe(' day');
expect(tenDays.suffix).toBe(' days');
});
});
@@ -25,7 +25,8 @@ describe('valueFormats', () => {
${'ms'} | ${4} | ${0.0024} | ${'0.0024 ms'}
${'ms'} | ${0} | ${100} | ${'100 ms'}
${'ms'} | ${2} | ${1250} | ${'1.25 s'}
${'ms'} | ${1} | ${10000086.123} | ${'2.8 hour'}
${'ms'} | ${1} | ${10000086.123} | ${'2.8 hours'}
${'ms'} | ${1} | ${-10000086.123} | ${'-2.8 hours'}
${'ms'} | ${undefined} | ${1000} | ${'1 s'}
${'ms'} | ${0} | ${1200} | ${'1 s'}
${'short'} | ${undefined} | ${1000} | ${'1 K'}
@@ -69,6 +70,7 @@ describe('valueFormats', () => {
${'dateTimeAsUS'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'07/02/2010 12:00:00 am'}
${'dateTimeAsSystem'} | ${0} | ${dateTime(new Date(2010, 6, 2)).valueOf()} | ${'2010-07-02 00:00:00'}
${'dtdurationms'} | ${undefined} | ${100000} | ${'1 minute'}
${'dtdurationms'} | ${undefined} | ${150000} | ${'2 minutes'}
`(
'With format=$format decimals=$decimals and value=$value then result shoudl be = $expected',
async ({ format, value, decimals, expected }) => {
@@ -102,10 +102,27 @@ function getDecimalsForValue(value: number): number {
export function toFixedScaled(value: number, decimals: DecimalCount, ext?: string): FormattedValue {
return {
text: toFixed(value, decimals),
suffix: ext,
suffix: appendPluralIf(ext, Math.abs(value) > 1),
};
}
function appendPluralIf(ext: string | undefined, condition: boolean): string | undefined {
if (!condition) {
return ext;
}
switch (ext) {
case ' min':
case ' hour':
case ' day':
case ' week':
case ' year':
return `${ext}s`;
default:
return ext;
}
}
export function toFixedUnit(unit: string, asPrefix?: boolean): ValueFormatter {
return (size: number, decimals?: DecimalCount) => {
if (size === null) {
+1 -1
View File
@@ -41,7 +41,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-node-resolve": "15.2.3",
"@types/node": "18.18.4",
"@types/node": "20.8.10",
"esbuild": "0.18.12",
"rimraf": "5.0.1",
"rollup": "2.79.1",
@@ -34,9 +34,14 @@ export interface GrafanaPyroscopeDataQuery extends common.DataQuery {
* Specifies the type of profile to query.
*/
profileTypeId: string;
/**
* Specifies the query span selectors.
*/
spanSelector?: Array<string>;
}
export const defaultGrafanaPyroscopeDataQuery: Partial<GrafanaPyroscopeDataQuery> = {
groupBy: [],
labelSelector: '{}',
spanSelector: [],
};
+2 -2
View File
@@ -134,7 +134,7 @@
"@storybook/react-webpack5": "7.4.5",
"@storybook/theming": "7.4.5",
"@testing-library/dom": "9.3.3",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/jest-dom": "6.1.4",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.5.1",
"@types/common-tags": "^1.8.0",
@@ -145,7 +145,7 @@
"@types/jquery": "3.5.16",
"@types/lodash": "4.14.195",
"@types/mock-raf": "1.0.3",
"@types/node": "18.18.4",
"@types/node": "20.8.10",
"@types/prismjs": "1.26.0",
"@types/react": "18.2.15",
"@types/react-beautiful-dnd": "13.1.4",
@@ -39,7 +39,7 @@ export const ContextMenu = React.memo(
setPositionStyles({
position: 'fixed',
left: collisions.right ? x - rect.width - OFFSET : x - OFFSET,
top: collisions.bottom ? y - rect.height - OFFSET : y + OFFSET,
top: Math.max(0, collisions.bottom ? y - rect.height - OFFSET : y + OFFSET),
});
}
}, [x, y]);
@@ -110,6 +110,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme2, size: RadioBut
radioLabel: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize,
height: `${labelHeight}px`,
// Deduct border from line-height for perfect vertical centering on windows and linux
@@ -8,13 +8,15 @@ import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/da
import { TooltipDisplayMode } from '@grafana/schema';
import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../VizTooltip';
import { FlotPosition } from '../VizTooltip/VizTooltip';
import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu';
import { GraphTooltip } from './GraphTooltip/GraphTooltip';
import { GraphDimensions } from './GraphTooltip/types';
import { FlotPosition, FlotItem } from './types';
import { FlotItem } from './types';
import { graphTimeFormat, graphTickFormatter } from './utils';
/** @deprecated */
export interface GraphProps {
ariaLabel?: string;
children?: JSX.Element | JSX.Element[];
@@ -31,6 +33,7 @@ export interface GraphProps {
onHorizontalRegionSelected?: (from: number, to: number) => void;
}
/** @deprecated */
interface GraphState {
pos?: FlotPosition;
contextPos?: FlotPosition;
@@ -21,8 +21,10 @@ import { SeriesIcon } from '../VizLegend/SeriesIcon';
import { GraphDimensions } from './GraphTooltip/types';
/** @deprecated */
export type ContextDimensions<T extends Dimensions = any> = { [key in keyof T]: [number, number | undefined] | null };
/** @deprecated */
export type GraphContextMenuProps = ContextMenuProps & {
getContextMenuSource: () => FlotDataPoint | null;
timeZone?: TimeZone;
@@ -3,22 +3,26 @@ import React, { Component } from 'react';
import { GraphSeriesXY } from '@grafana/data';
/** @deprecated */
export interface GraphSeriesTogglerAPI {
onSeriesToggle: (label: string, event: React.MouseEvent<HTMLElement>) => void;
toggledSeries: GraphSeriesXY[];
}
/** @deprecated */
export interface GraphSeriesTogglerProps {
children: (api: GraphSeriesTogglerAPI) => JSX.Element;
series: GraphSeriesXY[];
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
}
/** @deprecated */
export interface GraphSeriesTogglerState {
hiddenSeries: string[];
toggledSeries: GraphSeriesXY[];
}
/** @deprecated */
export class GraphSeriesToggler extends Component<GraphSeriesTogglerProps, GraphSeriesTogglerState> {
constructor(props: GraphSeriesTogglerProps) {
super(props);
@@ -8,6 +8,7 @@ import { MultiModeGraphTooltip } from './MultiModeGraphTooltip';
import { SingleModeGraphTooltip } from './SingleModeGraphTooltip';
import { GraphDimensions } from './types';
/** @deprecated */
export const GraphTooltip = ({
mode = TooltipDisplayMode.Single,
dimensions,
@@ -3,16 +3,18 @@ import React from 'react';
import { getValueFromDimension } from '@grafana/data';
import { SeriesTable } from '../../VizTooltip';
import { FlotPosition } from '../types';
import { FlotPosition } from '../../VizTooltip/VizTooltip';
import { getMultiSeriesGraphHoverInfo } from '../utils';
import { GraphTooltipContentProps } from './types';
/** @deprecated */
type Props = GraphTooltipContentProps & {
// We expect position to figure out correct values when not hovering over a datapoint
pos: FlotPosition;
};
/** @deprecated */
export const MultiModeGraphTooltip = ({ dimensions, activeDimensions, pos, timeZone }: Props) => {
let activeSeriesIndex: number | null = null;
// when no x-axis provided, skip rendering
@@ -11,6 +11,7 @@ import { SeriesTable } from '../../VizTooltip';
import { GraphTooltipContentProps } from './types';
/** @deprecated */
export const SingleModeGraphTooltip = ({ dimensions, activeDimensions, timeZone }: GraphTooltipContentProps) => {
// not hovering over a point, skip rendering
if (
@@ -2,11 +2,13 @@ import { Dimension, Dimensions, TimeZone } from '@grafana/data';
import { ActiveDimensions } from '../../VizTooltip';
/** @deprecated */
export interface GraphDimensions extends Dimensions {
xAxis: Dimension<number>;
yAxis: Dimension<number>;
}
/** @deprecated */
export interface GraphTooltipContentProps {
dimensions: GraphDimensions; // Dimension[]
activeDimensions: ActiveDimensions<GraphDimensions>;
@@ -1,12 +1,4 @@
export interface FlotPosition {
pageX: number;
pageY: number;
x: number;
x1: number;
y: number;
y1: number;
}
/** @deprecated */
export interface FlotItem<T> {
datapoint: [number, number];
dataIndex: number;
@@ -13,6 +13,7 @@ import {
*
* @param posX
* @param series
* @deprecated
*/
export const findHoverIndexFromData = (xAxisDimension: Field, xPos: number) => {
let lower = 0;
@@ -50,6 +51,7 @@ interface MultiSeriesHoverInfo {
*
* @param seriesList list of series visible on the Graph
* @param pos mouse cursor position, based on jQuery.flot position
* @deprecated
*/
export const getMultiSeriesGraphHoverInfo = (
// x and y axis dimensions order is aligned
@@ -102,6 +104,7 @@ export const getMultiSeriesGraphHoverInfo = (
};
};
/** @deprecated */
export const graphTickFormatter = (epoch: number, axis: any) => {
return dateTimeFormat(epoch, {
format: axis?.options?.timeformat,
@@ -109,6 +112,7 @@ export const graphTickFormatter = (epoch: number, axis: any) => {
});
};
/** @deprecated */
export const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => {
if (min && max && ticks) {
const range = max - min;
@@ -16,7 +16,7 @@ Use it whenever you would use custom CSS.
#### When not to use
If you need layout styles, use the Stack, Flex or Grid components instead.
If you need layout styles, use the Stack or Grid components instead.
### How to add a prop to Box
@@ -3,7 +3,7 @@ import React from 'react';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Text } from '../../Text/Text';
import { Flex } from '../Flex/Flex';
import { Stack } from '../Stack/Stack';
import { Box, BackgroundColor, BorderColor, BorderStyle, BorderRadius, BoxShadow } from './Box';
import mdx from './Box.mdx';
@@ -48,11 +48,11 @@ const Item = ({ background }: { background?: string }) => {
export const Basic: StoryFn<typeof Box> = (args) => {
return (
<Flex>
<Stack>
<Box borderColor="medium" {...args}>
Box
</Box>
</Flex>
</Stack>
);
};
@@ -88,64 +88,64 @@ Basic.args = {
export const Background: StoryFn<typeof Box> = () => {
return (
<Flex gap={4}>
<Stack gap={4}>
{backgroundOptions.map((background) => (
<Flex key={background} direction="column" alignItems="flex-start">
<Stack key={background} direction="column" alignItems="flex-start">
{background}
<Box backgroundColor={background} borderColor="strong" borderStyle="solid">
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
);
};
export const Border: StoryFn<typeof Box> = () => {
return (
<Flex direction="column" gap={4}>
<Stack direction="column" gap={4}>
<div>
<Text variant="h4">Border Color</Text>
<Flex gap={4} wrap="wrap">
<Stack gap={4} wrap="wrap">
{borderColorOptions.map((border) => (
<Flex key={border} direction="column" alignItems="flex-start">
<Stack key={border} direction="column" alignItems="flex-start">
{border}
<Box borderColor={border} borderStyle="solid">
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
</div>
<div>
<Text variant="h4">Border Style</Text>
<Flex gap={4} wrap="wrap">
<Stack gap={4} wrap="wrap">
{borderStyleOptions.map((border) => (
<Flex key={border} direction="column" alignItems="flex-start">
<Stack key={border} direction="column" alignItems="flex-start">
{border}
<Box borderColor="info" borderStyle={border}>
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
</div>
</Flex>
</Stack>
);
};
export const Shadow: StoryFn<typeof Box> = () => {
return (
<Flex gap={4}>
<Stack gap={4}>
{boxShadowOptions.map((shadow) => (
<Flex key={shadow} direction="column" alignItems="flex-start">
<Stack key={shadow} direction="column" alignItems="flex-start">
{shadow}
<Box boxShadow={shadow} borderColor="strong" borderStyle="solid">
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
);
};
@@ -4,7 +4,7 @@ import React, { ElementType, forwardRef, PropsWithChildren } from 'react';
import { GrafanaTheme2, ThemeSpacingTokens, ThemeShape, ThemeShadows } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { AlignItems, JustifyContent } from '../Flex/Flex';
import { AlignItems, JustifyContent } from '../Stack/Stack';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
type Display = 'flex' | 'block' | 'inline' | 'none';
@@ -1,202 +0,0 @@
import { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { useTheme2 } from '../../../themes';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Flex, JustifyContent, Wrap, Direction } from './Flex';
import mdx from './Flex.mdx';
const Item = ({ color, text, height }: { color: string; text?: string | number; height?: string }) => {
return (
<div
style={{
width: '5em',
height: height,
backgroundColor: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{text && <h3 style={{ color: 'black' }}>{text}</h3>}
</div>
);
};
const meta: Meta<typeof Flex> = {
title: 'General/Layout/Flex',
component: Flex,
parameters: {
docs: {
page: mdx,
},
},
};
export const Basic: StoryFn<typeof Flex> = ({ direction, wrap, alignItems, justifyContent, gap }) => {
const theme = useTheme2();
return (
<div style={{ width: '600px', height: '600px', border: '1px solid grey' }}>
<Flex direction={direction} wrap={wrap} alignItems={alignItems} justifyContent={justifyContent} gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</div>
);
};
Basic.argTypes = {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] },
wrap: { control: 'select', options: ['nowrap', 'wrap', 'wrap-reverse'] },
alignItems: {
control: 'select',
options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'],
},
justifyContent: {
control: 'select',
options: [
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'space-evenly',
'start',
'end',
'left',
'right',
],
},
};
export const AlignItemsExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
return (
<div style={{ width: '600px' }}>
<p>Align items flex-start</p>
<Flex direction="row" wrap="wrap" alignItems="flex-start" justifyContent="start" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items flex-end</p>
<Flex direction="row" wrap="wrap" alignItems="flex-end" justifyContent="end" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items baseline</p>
<Flex direction="row" wrap="nowrap" alignItems="baseline" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items center</p>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items stretch</p>
<Flex direction="row" wrap="wrap" alignItems="stretch" justifyContent="center" gap={2}>
<Item color={theme.colors.error.main} height="10em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} height="3em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} />
</Flex>
</div>
);
};
export const JustifyContentExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const justifyContentOptions: JustifyContent[] = [
'space-between',
'space-around',
'space-evenly',
'flex-start',
'flex-end',
'center',
];
return (
<div style={{ width: '600px' }}>
{justifyContentOptions.map((justifyContent) => (
<>
<p>Justify Content {justifyContent}</p>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent={justifyContent} gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export const GapExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const gapOptions: ThemeSpacingTokens[] = [2, 8, 10];
return (
<div style={{ width: '800px' }}>
{gapOptions.map((gap) => (
<>
<p>Gap with spacingToken set to {gap}</p>
<Flex direction="row" wrap="wrap" alignItems="flex-start" justifyContent="flex-start" gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export const WrapExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const wrapOptions: Wrap[] = ['nowrap', 'wrap', 'wrap-reverse'];
return (
<div style={{ width: '600px' }}>
{wrapOptions.map((wrap) => (
<>
<p>Wrap examples with {wrap} and gap set to spacingToken 2 (16px)</p>
<Flex direction="row" wrap={wrap} alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 10 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export const DirectionExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const directionOptions: Direction[] = ['row', 'row-reverse', 'column', 'column-reverse'];
return (
<div style={{ width: '600px' }}>
{directionOptions.map((direction) => (
<>
<p>Direction {direction}</p>
<Flex direction={direction} wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export default meta;
@@ -1,138 +0,0 @@
import { Meta, ArgTypes } from '@storybook/blocks';
import { Flex } from './Flex';
<Meta title="MDX|Flex" component={Flex} />
# Flex
The Flex Component aims at providing a more efficient way to lay out, align and distribute space among items in a container and
the decision to create it is to ensure consistency in design across Grafana.
### Usage
#### When to use
Use when in need to align components and small parts of the application. Use as parent container to wrap elements that you wish to align in a certain way.
Also:
* when working with one dimension layout
* to display the direction of the elements
* to set the elements to wrap
* to align items (vertically or horizontally)
#### When not to use
When you need to lay out bigger parts of the application or when you want to create page lay out.
Also:
* for complex grid layouts with various rows and columns
* bidirectional layouts
* complex nesting
* equal height columns
### Variants
Flex component has few variants that can be used based on the desired alignment you need for your case.
Some examples of how to use the Flex component can be seen below:
- AlignItems stretch
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items stretch and justify-content to be center</h1>
</header>
<Flex direction="row" wrap="wrap" alignItems="stretch" justifyContent="center" gap={2}>
<Item color={theme.colors.warning.main} height="10em" />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} height="3em" />
<Item color={theme.colors.warning.main} />
</Flex>
```
- Wrap items wrap-reverse
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with wrap-reverse property</h1>
</header>
<Flex direction="row" wrap="wrap-reverse" alignItems="center" justifyContent="center" gap={4}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
- JustifyContent flex-start
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with justify-content property</h1>
</header>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent="flex-start" gap={2}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
- Gap of 16px using the ThemeSpacingTokens
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with gap of 16px</h1>
</header>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
- Direction column
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with direction column</h1>
</header>
<Flex direction="column" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
### Props
<ArgTypes of={Flex} />
@@ -1,89 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
export type AlignItems =
| 'stretch'
| 'flex-start'
| 'flex-end'
| 'center'
| 'baseline'
| 'start'
| 'end'
| 'self-start'
| 'self-end';
export type JustifyContent =
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'start'
| 'end'
| 'left'
| 'right';
export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse';
export type Wrap = 'nowrap' | 'wrap' | 'wrap-reverse';
interface FlexProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
gap?: ResponsiveProp<ThemeSpacingTokens>;
alignItems?: ResponsiveProp<AlignItems>;
justifyContent?: ResponsiveProp<JustifyContent>;
direction?: ResponsiveProp<Direction>;
wrap?: ResponsiveProp<Wrap>;
children?: React.ReactNode;
}
export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
({ gap = 1, alignItems, justifyContent, direction, wrap, children, ...rest }, ref) => {
const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap);
return (
<div ref={ref} className={styles.flex} {...rest}>
{children}
</div>
);
}
);
Flex.displayName = 'Flex';
const getStyles = (
theme: GrafanaTheme2,
gap: FlexProps['gap'],
alignItems: FlexProps['alignItems'],
justifyContent: FlexProps['justifyContent'],
direction: FlexProps['direction'],
wrap: FlexProps['wrap']
) => {
return {
flex: css([
{
display: 'flex',
},
getResponsiveStyle<Direction>(theme, direction, (val) => ({
flexDirection: val,
})),
getResponsiveStyle<Wrap>(theme, wrap, (val) => ({
flexWrap: val,
})),
getResponsiveStyle<AlignItems>(theme, alignItems, (val) => ({
alignItems: val,
})),
getResponsiveStyle<JustifyContent>(theme, justifyContent, (val) => ({
justifyContent: val,
})),
getResponsiveStyle<ThemeSpacingTokens>(theme, gap, (val) => ({
gap: theme.spacing(val),
})),
]),
};
};
@@ -17,11 +17,6 @@ Use the Grid component when you want to create structured and organized layouts
Use the `Stack` component instead for these use cases:
- **Simple layouts:** When you need to arrange elements in a linear format, either vertically or horizontally.
- **Regular flow:** When you want a "regular" site flow but with standardized spacing between the elements.
Use the `Flex` component instead for these use cases:
- **Alignment:** More options for item alignment.
- **Flex items:** Custom flex basis or configure how items stretch and wrap.
@@ -1,20 +0,0 @@
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { ResponsiveProp } from '../utils/responsiveness';
import { Stack } from './Stack';
interface HorizontalStackProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
gap?: ResponsiveProp<ThemeSpacingTokens>;
}
export const HorizontalStack = React.forwardRef<HTMLDivElement, React.PropsWithChildren<HorizontalStackProps>>(
({ children, gap = 1, ...rest }, ref) => (
<Stack ref={ref} direction="row" gap={gap} {...rest}>
{children}
</Stack>
)
);
HorizontalStack.displayName = 'HorizontalStack';
@@ -1,16 +1,31 @@
import { Meta, StoryFn } from '@storybook/react';
import React, { ReactNode } from 'react';
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { useTheme2 } from '../../../themes';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Alert } from '../../Alert/Alert';
import { Button } from '../../Button';
import { Card } from '../../Card/Card';
import { Text } from '../../Text/Text';
import { HorizontalStack } from './HorizontalStack';
import { Stack } from './Stack';
import { Stack, JustifyContent, Wrap, Direction } from './Stack';
import mdx from './Stack.mdx';
const Item = ({ color, text, height }: { color: string; text?: string | number; height?: string }) => {
return (
<div
style={{
width: '5em',
height: height,
backgroundColor: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{text && <h3 style={{ color: 'black' }}>{text}</h3>}
</div>
);
};
const meta: Meta<typeof Stack> = {
title: 'General/Layout/Stack',
component: Stack,
@@ -19,204 +34,169 @@ const meta: Meta<typeof Stack> = {
page: mdx,
},
},
argTypes: {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'column'] },
},
};
const Item = ({ children }: { children: ReactNode }) => (
<div style={{ backgroundColor: 'lightgrey', width: '100px', height: '50px' }}>{children}</div>
);
export const Basic: StoryFn<typeof Stack> = ({ direction = 'column', gap = 2 }) => {
export const Basic: StoryFn<typeof Stack> = ({ direction, wrap, alignItems, justifyContent, gap }) => {
const theme = useTheme2();
return (
<Stack direction={direction} gap={gap}>
<Item>Item 1</Item>
<Item>Item 2</Item>
<Item>Item 3</Item>
</Stack>
);
};
export const TestCases: StoryFn<typeof Stack> = () => {
return (
<div style={{ width: '100%' }}>
<Stack gap={4}>
<h2>Comparisons Stack vs No stack</h2>
<HorizontalStack>
<Example title="No stack">
<Button>A button</Button>
<Button>Longer button button</Button>
</Example>
<Example title="Horizontal stack">
<HorizontalStack>
<Button>A button</Button>
<Button>Longer button button</Button>
</HorizontalStack>
</Example>
<Example title="Vertical stack">
<Stack>
<Button>A button</Button>
<Button>Longer button button</Button>
</Stack>
</Example>
</HorizontalStack>
<HorizontalStack>
<Example title="No stack, mismatched heights">
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description and some actions</Card.Description>
<Card.Actions>
<Button variant="secondary">Settings</Button>
<Button variant="secondary">Explore</Button>
</Card.Actions>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
<Button>Please press me!</Button>
</Example>
<Example title="Vertical stack, mismatched heights">
<Stack>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description and some actions</Card.Description>
<Card.Actions>
<Button variant="secondary">Settings</Button>
<Button variant="secondary">Explore</Button>
</Card.Actions>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
<Button>Please press me!</Button>
</Stack>
</Example>
</HorizontalStack>
<div style={{ width: 500 }}>
<Example title="No stack, too many items">
<Button>A button</Button>
<Button>Longer button button</Button>
<Button>Another button</Button>
<Button>And another</Button>
<Button>Why not - one last button!</Button>
</Example>
<Example title="Horizontal stack, too many items">
<HorizontalStack>
<Button>A button</Button>
<Button>Longer button button</Button>
<Button>Another button</Button>
<Button>And another</Button>
<Button>Why not - one last button!</Button>
</HorizontalStack>
</Example>
</div>
<h2>Child alignment</h2>
<div style={{ width: 500 }}>
<Example title="Row, mismatched heights">
<HorizontalStack>
<MyComponent>
<div style={{ height: 50, width: 100, background: 'blue' }} />
</MyComponent>
<MyComponent>
<div style={{ height: 150, width: 100, background: 'orange' }} />
</MyComponent>
</HorizontalStack>
</Example>
</div>
<Example title="Horizontal stack, mismatched heights">
<HorizontalStack>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description and some actions</Card.Description>
<Card.Actions>
<Button variant="secondary">Settings</Button>
<Button variant="secondary">Explore</Button>
</Card.Actions>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
</HorizontalStack>
</Example>
<Example title="Horizontal stack, mismatched heights with different components">
<HorizontalStack>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
<Alert severity="info" title="Plus an alert!" />
</HorizontalStack>
</Example>
<Example title="Horizontal stack, alerts with even heights">
<HorizontalStack>
<Alert severity="info" title="Plus an alert!" />
<Alert severity="success" title="Plus an alert!" />
<Alert severity="warning" title="Plus an alert!" />
<Alert severity="error" title="Plus an alert!" />
</HorizontalStack>
</Example>
<Example title="Horizontal stack, alerts with mismatched heights">
<HorizontalStack>
<Alert severity="info" title="Plus an alert!" />
<Alert severity="success" title="Plus an alert!" />
<Alert severity="warning" title="Plus an alert!">
Surprise - a description! What will happen to the height of all the other alerts?
</Alert>
<Alert severity="error" title="Plus an alert!" />
</HorizontalStack>
</Example>
<div style={{ width: '600px', height: '600px', border: '1px solid grey' }}>
<Stack direction={direction} wrap={wrap} alignItems={alignItems} justifyContent={justifyContent} gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</div>
);
};
function Example({ title, children }: { title: string; children: React.ReactNode }) {
Basic.argTypes = {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] },
wrap: { control: 'select', options: ['nowrap', 'wrap', 'wrap-reverse'] },
alignItems: {
control: 'select',
options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'],
},
justifyContent: {
control: 'select',
options: [
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'space-evenly',
'start',
'end',
'left',
'right',
],
},
};
export const AlignItemsExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
return (
<div>
<Text variant="h3">{title}</Text>
<div style={{ background: 'rgba(255,255,255,0.1)', border: '1px dashed green' }}>{children}</div>
<div style={{ width: '600px' }}>
<p>Align items flex-start</p>
<Stack direction="row" wrap="wrap" alignItems="flex-start" justifyContent="start" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items flex-end</p>
<Stack direction="row" wrap="wrap" alignItems="flex-end" justifyContent="end" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items baseline</p>
<Stack direction="row" wrap="nowrap" alignItems="baseline" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items center</p>
<Stack direction="row" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items stretch</p>
<Stack direction="row" wrap="wrap" alignItems="stretch" justifyContent="center" gap={2}>
<Item color={theme.colors.error.main} height="10em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} height="3em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} />
</Stack>
</div>
);
}
};
function MyComponent({ children }: { children: React.ReactNode }) {
return <div style={{ background: 'rgba(0,255,255, 0.2)', padding: 16 }}>{children}</div>;
}
export const JustifyContentExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const justifyContentOptions: JustifyContent[] = [
'space-between',
'space-around',
'space-evenly',
'flex-start',
'flex-end',
'center',
];
return (
<div style={{ width: '600px' }}>
{justifyContentOptions.map((justifyContent) => (
<>
<p>Justify Content {justifyContent}</p>
<Stack direction="row" wrap="wrap" alignItems="center" justifyContent={justifyContent} gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export const GapExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const gapOptions: ThemeSpacingTokens[] = [2, 8, 10];
return (
<div style={{ width: '800px' }}>
{gapOptions.map((gap) => (
<>
<p>Gap with spacingToken set to {gap}</p>
<Stack direction="row" wrap="wrap" alignItems="flex-start" justifyContent="flex-start" gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export const WrapExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const wrapOptions: Wrap[] = ['nowrap', 'wrap', 'wrap-reverse'];
return (
<div style={{ width: '600px' }}>
{wrapOptions.map((wrap) => (
<>
<p>Wrap examples with {wrap} and gap set to spacingToken 2 (16px)</p>
<Stack direction="row" wrap={wrap} alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 10 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export const DirectionExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const directionOptions: Direction[] = ['row', 'row-reverse', 'column', 'column-reverse'];
return (
<div style={{ width: '600px' }}>
{directionOptions.map((direction) => (
<>
<p>Direction {direction}</p>
<Stack direction={direction} wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export default meta;
@@ -1,44 +1,27 @@
import { Meta, ArgTypes, Canvas } from '@storybook/blocks';
import { Stack, HorizontalStack } from './index';
import * as Stories from './Stack.internal.story';
import { Meta, ArgTypes } from '@storybook/blocks';
import { Stack } from './Stack';
<Meta title="MDX|Stack" component={Stack} />
# Stack
The `Stack` component is designed to assist with layout and positioning of elements within a container, offering a simple and flexible way to stack elements vertically or horizontally. This documentation outlines the proper usage of the Stack component and provides guidance on when to use it over the Grid or Flex components.
There is also a `HorizontalStack` component, which is a thin wrapper around Stack, equivalent to `<Stack direction="row">`.
The Stack component is a simple wrapper around the flexbox layout model that allows to easily create responsive and flexible layouts. It provides a simple and intuitive way to align and distribute items within a container either horizontally or vertically.
### Usage
#### When to use
Use the Stack component when you need to arrange elements in a linear format, either vertically or horizontally. It's particularly useful when you want to maintain consistent spacing between items.
Use the Stack component when:
- You need a simple way to stack elements with predictable spacing.
- You want to ensure consistent alignment.
- For creating responsive and flexible layouts that can adapt to different screen sizes and orientations.
- When needing a simple and intuitive way to align and distribute items within a container either horizontally or vertically.
- To easily reorder and rearrange elements without changing the HTML structure.
- When aiming to create equal height columns.
- To create a grid-like structure with automatic wrapping and sizing of items based on the available space.
#### When not to use
Use the `Grid` component instead for these use cases:
- For complex multi-dimensional layouts with intricate requirements that are better suited for CSS frameworks or grid systems.
- When precise control over spacing and positioning of elements is necessary.
- **Intricate Layouts:** Grids are ideal for complex dashboard and magazine-style designs with rows and columns.
- **Structured Grid:** Use Grids when items need to span multiple rows/columns, creating structured layouts.
Use the `Flex` component instead for these use cases:
- **Alignment:** More options for item alignment.
- **Flex items:** Custom flex basis or configure how items stretch and wrap.
## Props
### Stack
### Props
<ArgTypes of={Stack} />
### HorizontalStack
<ArgTypes of={HorizontalStack} />
@@ -1,26 +1,89 @@
import { css } from '@emotion/css';
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
export type AlignItems =
| 'stretch'
| 'flex-start'
| 'flex-end'
| 'center'
| 'baseline'
| 'start'
| 'end'
| 'self-start'
| 'self-end';
export type JustifyContent =
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'start'
| 'end'
| 'left'
| 'right';
export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse';
export type Wrap = 'nowrap' | 'wrap' | 'wrap-reverse';
import { Flex } from '../Flex/Flex';
import { ResponsiveProp } from '../utils/responsiveness';
interface StackProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
direction?: ResponsiveProp<'column' | 'row'>;
gap?: ResponsiveProp<ThemeSpacingTokens>;
alignItems?: ResponsiveProp<AlignItems>;
justifyContent?: ResponsiveProp<JustifyContent>;
direction?: ResponsiveProp<Direction>;
wrap?: ResponsiveProp<Wrap>;
children?: React.ReactNode;
}
export const Stack = React.forwardRef<HTMLDivElement, React.PropsWithChildren<StackProps>>(
({ gap = 1, direction = 'column', children, ...rest }, ref) => {
export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
({ gap = 1, alignItems, justifyContent, direction, wrap, children, ...rest }, ref) => {
const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap);
return (
<Flex ref={ref} gap={gap} direction={direction} wrap="wrap" {...rest}>
{React.Children.toArray(children)
.filter(Boolean)
.map((child, index) => (
<div key={index}>{child}</div>
))}
</Flex>
<div ref={ref} className={styles.flex} {...rest}>
{children}
</div>
);
}
);
Stack.displayName = 'Stack';
const getStyles = (
theme: GrafanaTheme2,
gap: StackProps['gap'],
alignItems: StackProps['alignItems'],
justifyContent: StackProps['justifyContent'],
direction: StackProps['direction'],
wrap: StackProps['wrap']
) => {
return {
flex: css([
{
display: 'flex',
},
getResponsiveStyle<Direction>(theme, direction, (val) => ({
flexDirection: val,
})),
getResponsiveStyle<Wrap>(theme, wrap, (val) => ({
flexWrap: val,
})),
getResponsiveStyle<AlignItems>(theme, alignItems, (val) => ({
alignItems: val,
})),
getResponsiveStyle<JustifyContent>(theme, justifyContent, (val) => ({
justifyContent: val,
})),
getResponsiveStyle<ThemeSpacingTokens>(theme, gap, (val) => ({
gap: theme.spacing(val),
})),
]),
};
};
@@ -1,2 +0,0 @@
export { Stack } from './Stack';
export { HorizontalStack } from './HorizontalStack';
@@ -465,7 +465,7 @@ const getStyles = (theme: GrafanaTheme2) => {
position: 'absolute',
left: 0,
top: 0,
zIndex: theme.zIndex.tooltip,
zIndex: 1,
}),
rightActions: css({
display: 'flex',
@@ -33,7 +33,7 @@ const SegmentFrame = ({
children: React.ReactNode;
}) => (
<>
<SegmentSection label="Segment Name">
<SegmentSection label="Segment">
{children}
<Segment Component={AddButton} onChange={({ value }) => action('New value added')(value)} options={options} />
</SegmentSection>
@@ -29,12 +29,13 @@ const SegmentFrame = ({
loadOptions: (options: Array<SelectableValue<string>>) => Promise<Array<SelectableValue<string>>>;
}>) => (
<>
<SegmentSection label="Segment Name">
<SegmentSection label="Segment">
{children}
<SegmentAsync
Component={AddButton}
onChange={(value) => action('New value added')(value)}
loadOptions={() => loadOptions(options)}
inputMinWidth={100}
/>
</SegmentSection>
</>
@@ -8,7 +8,7 @@ import { SegmentInputProps } from './SegmentInput';
const SegmentFrame = ({ children }: React.PropsWithChildren) => (
<>
<SegmentSection label="Segment Name">{children}</SegmentSection>
<SegmentSection label="Segment">{children}</SegmentSection>
</>
);
@@ -149,8 +149,8 @@ Toggletip.displayName = 'Toggletip';
export const getStyles = (theme: GrafanaTheme2) => {
const info = buildTooltipTheme(
theme,
theme.components.tooltip.background,
theme.components.tooltip.background,
theme.colors.background.primary,
theme.colors.border.weak,
theme.components.tooltip.text,
{ topBottom: 2, rightLeft: 2 }
);
@@ -0,0 +1,69 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, GraphSeriesValue } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { HorizontalGroup } from '../Layout/Layout';
import { VizTooltipColorIndicator } from './VizTooltipColorIndicator';
import { ColorIndicator } from './types';
export interface SeriesListProps {
series: SingleSeriesProps[];
}
// Based on SeriesTable, with new styling
export const SeriesList = ({ series }: SeriesListProps) => {
return (
<>
{series.map((series, index) => {
return (
<SingleSeries
isActive={series.isActive}
label={series.label}
color={series.color}
value={series.value}
key={`${series.label}-${index}`}
/>
);
})}
</>
);
};
export interface SingleSeriesProps {
color?: string;
label?: React.ReactNode;
value?: string | GraphSeriesValue;
isActive?: boolean;
colorIndicator?: ColorIndicator;
}
const SingleSeries = ({ label, value, color, colorIndicator = ColorIndicator.series, isActive }: SingleSeriesProps) => {
const styles = useStyles2(getStyles);
return (
<HorizontalGroup justify="space-between" spacing="md" className={styles.hgContainer}>
<>
{color && <VizTooltipColorIndicator color={color} colorIndicator={colorIndicator} />}
{label && <div className={cx(styles.label, isActive && styles.activeSeries)}>{label}</div>}
</>
{value && <div className={cx(isActive && styles.activeSeries)}>{value}</div>}
</HorizontalGroup>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
hgContainer: css({
flexGrow: 1,
}),
activeSeries: css({
fontWeight: theme.typography.fontWeightBold,
color: theme.colors.text.maxContrast,
}),
label: css({
color: theme.colors.text.secondary,
fontWeight: 400,
}),
});
@@ -5,11 +5,19 @@ import { Dimensions, TimeZone } from '@grafana/data';
import { TooltipDisplayMode } from '@grafana/schema';
import { useStyles2 } from '../../themes';
import { FlotPosition } from '../Graph/types';
import { Portal } from '../Portal/Portal';
import { VizTooltipContainer } from './VizTooltipContainer';
export interface FlotPosition {
pageX: number;
pageY: number;
x: number;
x1: number;
y: number;
y1: number;
}
// Describes active dimensions user interacts with
// It's a key-value pair where:
// - key is the name of the dimension
@@ -0,0 +1,64 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { ColorIndicator } from './types';
import { getColorIndicatorClass } from './utils';
interface Props {
color: string;
colorIndicator: ColorIndicator;
}
export type ColorIndicatorStyles = ReturnType<typeof getStyles>;
export const VizTooltipColorIndicator = ({ color, colorIndicator = ColorIndicator.value }: Props) => {
const styles = useStyles2(getStyles);
return (
<span
style={{ backgroundColor: color }}
className={cx(styles.colorIndicator, getColorIndicatorClass(colorIndicator, styles))}
/>
);
};
// @TODO Update classes/add svgs
const getStyles = (theme: GrafanaTheme2) => ({
colorIndicator: css({
marginRight: theme.spacing(0.5),
}),
series: css({
width: '14px',
height: '4px',
borderRadius: theme.shape.radius.pill,
}),
value: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.default,
fontWeight: 500,
}),
hexagon: css({}),
pie_1_4: css({}),
pie_2_4: css({}),
pie_3_4: css({}),
marker_sm: css({
width: '4px',
height: '4px',
borderRadius: theme.shape.radius.circle,
}),
marker_md: css({
width: '8px',
height: '8px',
borderRadius: theme.shape.radius.circle,
}),
marker_lg: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.circle,
}),
});
@@ -1,4 +1,4 @@
import { css, cx } from '@emotion/css';
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
@@ -6,15 +6,13 @@ import { GrafanaTheme2 } from '@grafana/data';
import { HorizontalGroup } from '..';
import { useStyles2 } from '../../themes';
import { VizTooltipColorIndicator } from './VizTooltipColorIndicator';
import { LabelValue } from './types';
import { getColorIndicatorClass } from './utils';
interface Props {
keyValuePairs?: LabelValue[];
}
export type HeaderLabelValueStyles = ReturnType<typeof getStyles>;
export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => {
const styles = useStyles2(getStyles);
@@ -25,10 +23,9 @@ export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => {
<HorizontalGroup justify="space-between" spacing="md" className={styles.hgContainer} key={i}>
<div className={styles.label}>{keyValuePair.label}</div>
<>
<span
style={{ backgroundColor: keyValuePair.color }}
className={cx(styles.colorIndicator, getColorIndicatorClass(keyValuePair.colorIndicator!, styles))}
/>
{keyValuePair.color && (
<VizTooltipColorIndicator color={keyValuePair.color} colorIndicator={keyValuePair.colorIndicator!} />
)}
{keyValuePair.value}
</>
</HorizontalGroup>
@@ -38,46 +35,12 @@ export const VizTooltipHeaderLabelValue = ({ keyValuePairs }: Props) => {
);
};
// @TODO Update classes/add svgs?
const getStyles = (theme: GrafanaTheme2) => ({
hgContainer: css({
flexGrow: 1,
}),
colorIndicator: css({
marginRight: theme.spacing(0.5),
}),
label: css({
color: theme.colors.text.secondary,
fontWeight: 400,
}),
series: css({
width: '14px',
height: '4px',
borderRadius: theme.shape.radius.pill,
}),
value: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.default,
fontWeight: 500,
}),
hexagon: css({}),
pie_1_4: css({}),
pie_2_4: css({}),
pie_3_4: css({}),
marker_sm: css({
width: '4px',
height: '4px',
borderRadius: theme.shape.radius.circle,
}),
marker_md: css({
width: '8px',
height: '8px',
borderRadius: theme.shape.radius.circle,
}),
marker_lg: css({
width: '12px',
height: '12px',
borderRadius: theme.shape.radius.circle,
}),
});
@@ -1,4 +1,4 @@
import { HeaderLabelValueStyles } from './VizTooltipHeaderLabelValue';
import { ColorIndicatorStyles } from './VizTooltipColorIndicator';
import { ColorIndicator } from './types';
export const calculateTooltipPosition = (
@@ -42,7 +42,7 @@ export const calculateTooltipPosition = (
return { x, y };
};
export const getColorIndicatorClass = (colorIndicator: string, styles: HeaderLabelValueStyles) => {
export const getColorIndicatorClass = (colorIndicator: string, styles: ColorIndicatorStyles) => {
switch (colorIndicator) {
case ColorIndicator.value:
return styles.value;
+1 -3
View File
@@ -9,7 +9,5 @@
* be subject to the standard policies
*/
export * from './components/Layout/Flex/Flex';
export { Grid } from './components/Layout/Grid/Grid';
export { Stack, HorizontalStack } from './components/Layout/Stack';
export { Stack } from './components/Layout/Stack/Stack';
+1 -1
View File
@@ -230,7 +230,7 @@ func TestAdminAPIEndpoint(t *testing.T) {
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
require.NoError(t, err)
assert.Equal(t, "user already exists", respJSON.Get("error").MustString())
assert.Equal(t, "User with email '' or username 'existing@example.com' already exists", respJSON.Get("message").MustString())
})
})
}
+1 -8
View File
@@ -499,14 +499,7 @@ func (hs *HTTPServer) registerRoutes() {
})
// Playlist
apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
playlistRoute.Get("/", routing.Wrap(hs.SearchPlaylists))
playlistRoute.Get("/:uid", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylist))
playlistRoute.Get("/:uid/items", hs.ValidateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems))
playlistRoute.Delete("/:uid", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.DeletePlaylist))
playlistRoute.Put("/:uid", reqEditorRole, hs.ValidateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist))
playlistRoute.Post("/", reqEditorRole, routing.Wrap(hs.CreatePlaylist))
})
hs.registerPlaylistAPI(apiRoute)
// Search
apiRoute.Get("/search/sorting", routing.Wrap(hs.ListSortOptions))
+11 -6
View File
@@ -521,12 +521,13 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
c.TimeRequest(metrics.MApiDashboardSave)
return response.JSON(http.StatusOK, util.DynMap{
"status": "success",
"slug": dashboard.Slug,
"version": dashboard.Version,
"id": dashboard.ID,
"uid": dashboard.UID,
"url": dashboard.GetURL(),
"status": "success",
"slug": dashboard.Slug,
"version": dashboard.Version,
"id": dashboard.ID,
"uid": dashboard.UID,
"url": dashboard.GetURL(),
"folderUid": dashboard.FolderUID,
})
}
@@ -1301,6 +1302,10 @@ type PostDashboardResponse struct {
// required: true
// example: /d/nHz3SXiiz/my-dashboard
URL string `json:"url"`
// FolderUID The unique identifier (uid) of the folder the dashboard belongs to.
// required: false
FolderUID string `json:"folderUid"`
} `json:"body"`
}
+3 -4
View File
@@ -3,7 +3,6 @@ package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
@@ -261,7 +260,7 @@ func TestSetFeatureToggles(t *testing.T) {
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, fmt.Sprintf("invalid toggle passed in: %s", featuremgmt.FlagFeatureToggleAdminPage), p["error"])
assert.Equal(t, "invalid toggle passed in", p["message"])
})
t.Run("because it is not GA or Deprecated", func(t *testing.T) {
@@ -274,7 +273,7 @@ func TestSetFeatureToggles(t *testing.T) {
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in: toggle2", p["error"])
assert.Equal(t, "invalid toggle passed in", p["message"])
})
t.Run("because it is configured to be read-only", func(t *testing.T) {
@@ -287,7 +286,7 @@ func TestSetFeatureToggles(t *testing.T) {
res := runSetScenario(t, features, updates, s, writePermissions, http.StatusBadRequest)
defer func() { require.NoError(t, res.Body.Close()) }()
p := readBody(t, res.Body)
assert.Equal(t, "invalid toggle passed in: toggle3", p["error"])
assert.Equal(t, "invalid toggle passed in", p["message"])
})
})
+1 -2
View File
@@ -4,10 +4,9 @@ import (
"context"
"fmt"
"net/http"
"slices"
"strings"
"golang.org/x/exp/slices"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
+5 -2
View File
@@ -16,6 +16,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/routing"
httpstatic "github.com/grafana/grafana/pkg/api/static"
@@ -205,6 +207,7 @@ type HTTPServer struct {
authnService authn.Service
starApi *starApi.API
promRegister prometheus.Registerer
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
}
type ServerOptions struct {
@@ -246,8 +249,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
accesscontrolService accesscontrol.Service, navTreeService navtree.Service,
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service,
starApi *starApi.API, promRegister prometheus.Registerer,
starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider,
) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
@@ -348,6 +350,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginsCDNService: pluginsCDNService,
starApi: starApi,
promRegister: promRegister,
clientConfigProvider: clientConfigProvider,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
+3 -3
View File
@@ -162,7 +162,7 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
var resObj secretsErrorResponseBody
err = json.Unmarshal(buf.Bytes(), &resObj)
require.NoError(t, err)
require.Equal(t, "unknown error", resObj.Error)
require.Equal(t, "", resObj.Error)
require.Contains(t, resObj.Message, "Secrets Plugin error:")
})
}
@@ -264,12 +264,12 @@ func TestDataSourceQueryError(t *testing.T) {
{
request: reqDatasourceByUidNotFound,
expectedStatus: http.StatusNotFound,
expectedBody: `{"error":"data source not found","message":"Data source not found","traceID":""}`,
expectedBody: `{"message":"Data source not found","traceID":""}`,
},
{
request: reqDatasourceByIdNotFound,
expectedStatus: http.StatusNotFound,
expectedBody: `{"error":"data source not found","message":"Data source not found","traceID":""}`,
expectedBody: `{"message":"Data source not found","traceID":""}`,
},
}
+131 -1
View File
@@ -2,15 +2,145 @@ package api
import (
"net/http"
"strings"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
internalplaylist "github.com/grafana/grafana/pkg/apis/playlist"
"github.com/grafana/grafana/pkg/middleware"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
"github.com/grafana/grafana/pkg/web"
)
func (hs *HTTPServer) ValidateOrgPlaylist(c *contextmodel.ReqContext) {
type playlistAPIHandler struct {
SearchPlaylists []web.Handler
GetPlaylist []web.Handler
GetPlaylistItems []web.Handler
DeletePlaylist []web.Handler
UpdatePlaylist []web.Handler
CreatePlaylist []web.Handler
}
func chainHandlers(h ...web.Handler) []web.Handler {
return h
}
func (hs *HTTPServer) registerPlaylistAPI(apiRoute routing.RouteRegister) {
handler := playlistAPIHandler{
SearchPlaylists: chainHandlers(routing.Wrap(hs.SearchPlaylists)),
GetPlaylist: chainHandlers(hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylist)),
GetPlaylistItems: chainHandlers(hs.validateOrgPlaylist, routing.Wrap(hs.GetPlaylistItems)),
DeletePlaylist: chainHandlers(middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.DeletePlaylist)),
UpdatePlaylist: chainHandlers(middleware.ReqEditorRole, hs.validateOrgPlaylist, routing.Wrap(hs.UpdatePlaylist)),
CreatePlaylist: chainHandlers(middleware.ReqEditorRole, routing.Wrap(hs.CreatePlaylist)),
}
// Alternative implementations for k8s
if hs.Features.IsEnabled(featuremgmt.FlagKubernetesPlaylistsAPI) {
namespacer := request.GetNamespaceMapper(hs.Cfg)
gvr := schema.GroupVersionResource{
Group: internalplaylist.GroupName,
Version: internalplaylist.VersionID,
Resource: "playlists",
}
clientGetter := func(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) {
dyn, err := dynamic.NewForConfig(hs.clientConfigProvider.GetDirectRestConfig(c))
if err != nil {
c.JsonApiErr(500, "client", err)
return nil, false
}
return dyn.Resource(gvr).Namespace(namespacer(c.OrgID)), true
}
errorWriter := func(c *contextmodel.ReqContext, err error) {
//nolint:errorlint
statusError, ok := err.(*errors.StatusError)
if ok {
c.JsonApiErr(int(statusError.Status().Code),
statusError.Status().Message, err)
return
}
errhttp.Write(c.Req.Context(), err, c.Resp)
}
handler.SearchPlaylists = []web.Handler{func(c *contextmodel.ReqContext) {
client, ok := clientGetter(c)
if !ok {
return // error is already sent
}
out, err := client.List(c.Req.Context(), v1.ListOptions{})
if err != nil {
errorWriter(c, err)
return
}
query := strings.ToUpper(c.Query("query"))
playlists := []playlist.Playlist{}
for _, item := range out.Items {
p := internalplaylist.UnstructuredToLegacyPlaylist(item)
if p == nil {
continue
}
if query != "" && !strings.Contains(strings.ToUpper(p.Name), query) {
continue // query filter
}
playlists = append(playlists, *p)
}
c.JSON(http.StatusOK, playlists)
}}
handler.GetPlaylist = []web.Handler{func(c *contextmodel.ReqContext) {
client, ok := clientGetter(c)
if !ok {
return // error is already sent
}
uid := web.Params(c.Req)[":uid"]
out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
if err != nil {
errorWriter(c, err)
return
}
c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out))
}}
handler.GetPlaylistItems = []web.Handler{func(c *contextmodel.ReqContext) {
client, ok := clientGetter(c)
if !ok {
return // error is already sent
}
uid := web.Params(c.Req)[":uid"]
out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
if err != nil {
errorWriter(c, err)
return
}
c.JSON(http.StatusOK, internalplaylist.UnstructuredToLegacyPlaylistDTO(*out).Items)
}}
}
// Register the actual handlers
apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
playlistRoute.Get("/", handler.SearchPlaylists...)
playlistRoute.Get("/:uid", handler.GetPlaylist...)
playlistRoute.Get("/:uid/items", handler.GetPlaylistItems...)
playlistRoute.Delete("/:uid", handler.DeletePlaylist...)
playlistRoute.Put("/:uid", handler.UpdatePlaylist...)
playlistRoute.Post("/", handler.CreatePlaylist...)
})
}
func (hs *HTTPServer) validateOrgPlaylist(c *contextmodel.ReqContext) {
uid := web.Params(c.Req)[":uid"]
query := playlist.GetPlaylistByUidQuery{UID: uid, OrgId: c.SignedInUser.GetOrgID()}
p, err := hs.playlistService.GetWithoutItems(c.Req.Context(), &query)
+1 -1
View File
@@ -128,7 +128,7 @@ func TestCallResource(t *testing.T) {
_, err = io.Copy(body, resp.Body)
require.NoError(t, err)
expectedBody := `{ "error": "something went wrong", "message": "Failed to call resource", "traceID": "" }`
expectedBody := `{ "message": "Failed to call resource", "traceID": "" }`
require.JSONEq(t, expectedBody, body.String())
require.NoError(t, resp.Body.Close())
require.Equal(t, 500, resp.StatusCode)
-7
View File
@@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
@@ -253,12 +252,6 @@ func Error(status int, message string, err error) *NormalResponse {
data["message"] = message
}
if err != nil {
if setting.Env != setting.Prod {
data["error"] = err.Error()
}
}
resp := JSON(status, data)
if err != nil {
+112
View File
@@ -0,0 +1,112 @@
package playlist
import (
"encoding/json"
"fmt"
"strconv"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"github.com/grafana/grafana/pkg/kinds"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/playlist"
)
func UnstructuredToLegacyPlaylist(item unstructured.Unstructured) *playlist.Playlist {
spec := item.Object["spec"].(map[string]any)
return &playlist.Playlist{
UID: item.GetName(),
Name: spec["title"].(string),
Interval: spec["interval"].(string),
Id: getLegacyID(&item),
}
}
func UnstructuredToLegacyPlaylistDTO(item unstructured.Unstructured) *playlist.PlaylistDTO {
spec := item.Object["spec"].(map[string]any)
dto := &playlist.PlaylistDTO{
Uid: item.GetName(),
Name: spec["title"].(string),
Interval: spec["interval"].(string),
Id: getLegacyID(&item),
}
items := spec["items"]
if items != nil {
b, err := json.Marshal(items)
if err == nil {
_ = json.Unmarshal(b, &dto.Items)
}
}
return dto
}
func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceMapper) *Playlist {
spec := Spec{
Title: v.Name,
Interval: v.Interval,
}
for _, item := range v.Items {
spec.Items = append(spec.Items, Item{
Type: ItemType(item.Type),
Value: item.Value,
})
}
meta := kinds.GrafanaResourceMetadata{}
meta.SetUpdatedTimestampMillis(v.UpdatedAt)
if v.Id > 0 {
meta.SetOriginInfo(&kinds.ResourceOriginInfo{
Name: "SQL",
Key: fmt.Sprintf("%d", v.Id),
})
}
return &Playlist{
ObjectMeta: metav1.ObjectMeta{
Name: v.Uid,
UID: types.UID(v.Uid),
ResourceVersion: fmt.Sprintf("%d", v.UpdatedAt),
CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)),
Namespace: namespacer(v.OrgID),
Annotations: meta.Annotations,
},
Spec: spec,
}
}
func convertToLegacyUpdateCommand(p *Playlist, orgId int64) (*playlist.UpdatePlaylistCommand, error) {
spec := p.Spec
cmd := &playlist.UpdatePlaylistCommand{
UID: p.Name,
Name: spec.Title,
Interval: spec.Interval,
OrgId: orgId,
}
for _, item := range spec.Items {
if item.Type == ItemTypeDashboardById {
return nil, fmt.Errorf("unsupported item type: %s", item.Type)
}
cmd.Items = append(cmd.Items, playlist.PlaylistItem{
Type: string(item.Type),
Value: item.Value,
})
}
return cmd, nil
}
// Read legacy ID from metadata annotations
func getLegacyID(item *unstructured.Unstructured) int64 {
meta := kinds.GrafanaResourceMetadata{
Annotations: item.GetAnnotations(),
}
info := meta.GetOriginInfo()
if info != nil && info.Name == "SQL" {
i, err := strconv.ParseInt(info.Key, 10, 64)
if err == nil {
return i
}
}
return 0
}
@@ -1,4 +1,4 @@
package v0alpha1
package playlist
import (
"encoding/json"
@@ -12,6 +12,7 @@ import (
func TestPlaylistConversion(t *testing.T) {
src := &playlist.PlaylistDTO{
Id: 123,
OrgID: 3,
Uid: "abc", // becomes k8s name
Name: "MyPlaylists", // becomes title
@@ -32,14 +33,19 @@ func TestPlaylistConversion(t *testing.T) {
out, err := json.MarshalIndent(dst, "", " ")
require.NoError(t, err)
//fmt.Printf("%s", string(out))
// fmt.Printf("%s", string(out))
require.JSONEq(t, `{
"metadata": {
"name": "abc",
"namespace": "org-3",
"uid": "abc",
"resourceVersion": "54321",
"creationTimestamp": "1970-01-01T00:00:12Z"
"creationTimestamp": "1970-01-01T00:00:12Z",
"annotations": {
"grafana.app/originKey": "123",
"grafana.app/originName": "SQL",
"grafana.app/updatedTimestamp": "1970-01-01T00:00:54Z"
}
},
"spec": {
"title": "MyPlaylists",
+4
View File
@@ -0,0 +1,4 @@
// +k8s:deepcopy-gen=package
// +groupName=playlist.grafana.app
package playlist // import "github.com/grafana/grafana/pkg/apis/playlist"
@@ -1,8 +1,9 @@
package v0alpha1
package playlist
import (
"context"
"errors"
"fmt"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
@@ -21,6 +22,9 @@ var (
_ rest.Getter = (*legacyStorage)(nil)
_ rest.Lister = (*legacyStorage)(nil)
_ rest.Storage = (*legacyStorage)(nil)
_ rest.Creater = (*legacyStorage)(nil)
_ rest.Updater = (*legacyStorage)(nil)
_ rest.GracefulDeleter = (*legacyStorage)(nil)
)
type legacyStorage struct {
@@ -110,3 +114,96 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
return convertToK8sResource(dto, s.namespacer), nil
}
func (s *legacyStorage) Create(ctx context.Context,
obj runtime.Object,
createValidation rest.ValidateObjectFunc,
options *metav1.CreateOptions,
) (runtime.Object, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
p, ok := obj.(*Playlist)
if !ok {
return nil, fmt.Errorf("expected playlist?")
}
cmd, err := convertToLegacyUpdateCommand(p, info.OrgID)
if err != nil {
return nil, err
}
out, err := s.service.Create(ctx, &playlist.CreatePlaylistCommand{
UID: p.Name,
Name: cmd.Name,
Interval: cmd.Interval,
Items: cmd.Items,
OrgId: cmd.OrgId,
})
if err != nil {
return nil, err
}
return s.Get(ctx, out.UID, nil)
}
func (s *legacyStorage) Update(ctx context.Context,
name string,
objInfo rest.UpdatedObjectInfo,
createValidation rest.ValidateObjectFunc,
updateValidation rest.ValidateObjectUpdateFunc,
forceAllowCreate bool,
options *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
created := false
old, err := s.Get(ctx, name, nil)
if err != nil {
return old, created, err
}
obj, err := objInfo.UpdatedObject(ctx, old)
if err != nil {
return old, created, err
}
p, ok := obj.(*Playlist)
if !ok {
return nil, created, fmt.Errorf("expected playlist after update")
}
cmd, err := convertToLegacyUpdateCommand(p, info.OrgID)
if err != nil {
return old, created, err
}
_, err = s.service.Update(ctx, cmd)
if err != nil {
return nil, false, err
}
r, err := s.Get(ctx, name, nil)
return r, created, err
}
// GracefulDeleter
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
v, err := s.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return v, false, err // includes the not-found error
}
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, false, err
}
p, ok := v.(*Playlist)
if !ok {
return v, false, fmt.Errorf("expected a playlist response from Get")
}
err = s.service.Delete(ctx, &playlist.DeletePlaylistCommand{
UID: name,
OrgId: info.OrgID,
})
return p, true, err // true is instant delete
}
+118
View File
@@ -0,0 +1,118 @@
package playlist
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
common "k8s.io/kube-openapi/pkg/common"
grafanaapiserver "github.com/grafana/grafana/pkg/services/grafana-apiserver"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
grafanarest "github.com/grafana/grafana/pkg/services/grafana-apiserver/rest"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/utils"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/setting"
)
// GroupName is the group name for this API.
const GroupName = "playlist.grafana.app"
const VersionID = runtime.APIVersionInternal
var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil)
// This is used just so wire has something unique to return
type PlaylistAPIBuilder struct {
service playlist.Service
namespacer request.NamespaceMapper
gv schema.GroupVersion
}
func RegisterAPIService(p playlist.Service,
apiregistration grafanaapiserver.APIRegistrar,
cfg *setting.Cfg,
) *PlaylistAPIBuilder {
builder := &PlaylistAPIBuilder{
service: p,
namespacer: request.GetNamespaceMapper(cfg),
gv: schema.GroupVersion{Group: GroupName, Version: VersionID},
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *PlaylistAPIBuilder) GetGroupVersion() schema.GroupVersion {
return b.gv
}
func (b *PlaylistAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(b.gv,
&Playlist{},
&PlaylistList{},
)
return nil
}
func (b *PlaylistAPIBuilder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory, // pointer?
optsGetter generic.RESTOptionsGetter,
) (*genericapiserver.APIGroupInfo, error) {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(GroupName, scheme, metav1.ParameterCodec, codecs)
storage := map[string]rest.Storage{}
legacyStore := &legacyStorage{
service: b.service,
namespacer: b.namespacer,
DefaultQualifiedResource: b.gv.WithResource("playlists").GroupResource(),
SingularQualifiedResource: b.gv.WithResource("playlist").GroupResource(),
}
legacyStore.tableConverter = utils.NewTableConverter(
legacyStore.DefaultQualifiedResource,
[]metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The playlist name"},
{Name: "Interval", Type: "string", Format: "string", Description: "How often the playlist will update"},
{Name: "Created At", Type: "date"},
},
func(obj runtime.Object) ([]interface{}, error) {
m, ok := obj.(*Playlist)
if !ok {
return nil, fmt.Errorf("expected playlist")
}
return []interface{}{
m.Name,
m.Spec.Title,
m.Spec.Interval,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
},
)
storage["playlists"] = legacyStore
// enable dual writes if a RESTOptionsGetter is provided
if optsGetter != nil {
store, err := newStorage(scheme, optsGetter, legacyStore)
if err != nil {
return nil, err
}
storage["playlists"] = grafanarest.NewDualWriter(legacyStore, store)
}
apiGroupInfo.VersionedResourcesStorageMap["v0alpha1"] = storage
return &apiGroupInfo, nil
}
func (b *PlaylistAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return nil // no custom OpenAPI definitions
}
func (b *PlaylistAPIBuilder) GetAPIRoutes() *grafanaapiserver.APIRoutes {
return nil // no custom API routes
}
@@ -1,4 +1,4 @@
package v0alpha1
package playlist
import (
"k8s.io/apimachinery/pkg/runtime"
+65
View File
@@ -0,0 +1,65 @@
package playlist
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Playlist struct {
metav1.TypeMeta `json:",inline"`
// Standard object's metadata
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec Spec `json:"spec,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type PlaylistList struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []Playlist `json:"items,omitempty"`
}
// Spec defines model for Spec.
type Spec struct {
// Name of the playlist.
Title string `json:"title"`
// Interval sets the time between switching views in a playlist.
Interval string `json:"interval"`
// The ordered list of items that the playlist will iterate over.
Items []Item `json:"items,omitempty"`
}
// Defines values for ItemType.
const (
ItemTypeDashboardByTag ItemType = "dashboard_by_tag"
ItemTypeDashboardByUid ItemType = "dashboard_by_uid"
// deprecated -- should use UID
ItemTypeDashboardById ItemType = "dashboard_by_id"
)
// Item defines model for Item.
type Item struct {
// Type of the item.
Type ItemType `json:"type"`
// Value depends on type and describes the playlist item.
//
// - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
// is not portable as the numerical identifier is non-deterministic between different instances.
// Will be replaced by dashboard_by_uid in the future. (deprecated)
// - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
// dashboards behind the tag will be added to the playlist.
// - dashboard_by_uid: The value is the dashboard UID
Value string `json:"value"`
}
// Type of the item.
type ItemType string
-35
View File
@@ -1,35 +0,0 @@
package v0alpha1
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/playlist"
)
func convertToK8sResource(v *playlist.PlaylistDTO, namespacer request.NamespaceMapper) *Playlist {
spec := Spec{
Title: v.Name,
Interval: v.Interval,
}
for _, item := range v.Items {
spec.Items = append(spec.Items, Item{
Type: ItemType(item.Type),
Value: item.Value,
})
}
return &Playlist{
ObjectMeta: metav1.ObjectMeta{
Name: v.Uid,
UID: types.UID(v.Uid),
ResourceVersion: fmt.Sprintf("%d", v.UpdatedAt),
CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)),
Namespace: namespacer(v.OrgID),
},
Spec: spec,
}
}

Some files were not shown because too many files have changed in this diff Show More