mirror of
https://github.com/grafana/grafana.git
synced 2026-01-08 13:22:08 +08:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89b365f8b1 | ||
|
|
ec552dd6b8 | ||
|
|
95b7cabe00 | ||
|
|
3b70755095 | ||
|
|
7068dddd94 | ||
|
|
38ec1ea5e3 | ||
|
|
43ea988ec2 | ||
|
|
ced2d228f7 | ||
|
|
98095a6af2 | ||
|
|
29c141d4c3 | ||
|
|
25382580fe | ||
|
|
7feaf4b32c | ||
|
|
5610aee184 | ||
|
|
e15ee039e1 | ||
|
|
c5409af19c | ||
|
|
8bd0d15170 | ||
|
|
f09a065020 | ||
|
|
6c1463e3ae | ||
|
|
65c0f4e787 | ||
|
|
c45b49cb92 | ||
|
|
11a69c499f | ||
|
|
f6dca21a46 | ||
|
|
c90230a194 | ||
|
|
fb9571b348 | ||
|
|
7fff085d21 | ||
|
|
3dac951598 | ||
|
|
79d76996aa | ||
|
|
1b3a248142 | ||
|
|
bfe04d4d1a | ||
|
|
dc1e759ce5 | ||
|
|
08d483e0bd | ||
|
|
05948043e8 | ||
|
|
3513179722 | ||
|
|
c652135724 | ||
|
|
426fab32eb | ||
|
|
0e8f0d4b4a | ||
|
|
7ab181383b | ||
|
|
e0cf9e4331 | ||
|
|
9b5269dc4f | ||
|
|
2e36890b56 | ||
|
|
d147f3d366 | ||
|
|
c0ddd089ef | ||
|
|
556bec41c9 | ||
|
|
b95b9f0c47 | ||
|
|
4e2446d3e3 | ||
|
|
2cbaab0e4c | ||
|
|
85383e9b43 | ||
|
|
d895cad92a | ||
|
|
000cfe1dfc | ||
|
|
c4756c394b | ||
|
|
f5d7e0ed92 | ||
|
|
e20a296176 | ||
|
|
14878bcf99 | ||
|
|
f1efd350e6 | ||
|
|
66ceacd98f | ||
|
|
7a543052ee | ||
|
|
7e02e2aef3 | ||
|
|
a4919a6d69 | ||
|
|
17d98d79d9 | ||
|
|
82e6fdfaf9 | ||
|
|
ff49b0de7e | ||
|
|
068a41c6a7 | ||
|
|
812c85602b | ||
|
|
2fff4175b4 | ||
|
|
6dc4223e0b | ||
|
|
946daeb01a | ||
|
|
280e796635 | ||
|
|
dc23aa9a0f | ||
|
|
eb9f63c715 | ||
|
|
4c3adeff7c | ||
|
|
a2263b9249 | ||
|
|
692bd0ac00 | ||
|
|
df10c952c5 | ||
|
|
0491f55ad6 | ||
|
|
f3ffc1a495 | ||
|
|
3a1ffd88d5 | ||
|
|
eda3fb190c | ||
|
|
129b74fe08 | ||
|
|
8ae02b4b7b | ||
|
|
0bb76df454 | ||
|
|
7e509a19f1 | ||
|
|
3de3eee655 | ||
|
|
1bcdaeb910 | ||
|
|
90a904fbd5 | ||
|
|
7899b5ae72 | ||
|
|
3c353ab1c1 | ||
|
|
e1bf7aa65e | ||
|
|
6a6e05bfa1 | ||
|
|
28b0af4476 | ||
|
|
72ac08cc7b | ||
|
|
90946b68a2 | ||
|
|
9521c90651 | ||
|
|
d6ca111109 | ||
|
|
200915fc91 | ||
|
|
3bd24136a0 | ||
|
|
47a04102ee | ||
|
|
f35ab2f79d | ||
|
|
e07f115f98 | ||
|
|
001179771e | ||
|
|
661c72d6b7 | ||
|
|
4e31338e94 | ||
|
|
d5d4685d20 | ||
|
|
d7e459fdee | ||
|
|
4db1f3b850 | ||
|
|
4fb033d525 | ||
|
|
ffa649e377 | ||
|
|
450ff445ea | ||
|
|
311b4b9b6f | ||
|
|
710124b5de | ||
|
|
61eb256882 | ||
|
|
7ec8550652 | ||
|
|
4890db3089 | ||
|
|
81b0dd7686 | ||
|
|
cb9df3bfdb | ||
|
|
a0eb08f01b |
@@ -1538,7 +1538,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/Table/utils.test.ts:5381": [
|
||||
"packages/grafana-ui/src/components/Table/utils.test.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, "Unexpected any. Specify a different type.", "2"],
|
||||
@@ -1548,19 +1548,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
|
||||
],
|
||||
"packages/grafana-ui/src/components/Table/utils.ts:5381": [
|
||||
"packages/grafana-ui/src/components/Table/utils.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
@@ -3997,12 +3987,6 @@ exports[`better eslint`] = {
|
||||
"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, "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"]
|
||||
],
|
||||
"public/app/features/geo/format/geohash.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@@ -5395,8 +5379,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
||||
@@ -87,7 +87,11 @@
|
||||
"ignoreNonDOM": true
|
||||
}
|
||||
],
|
||||
"jsx-a11y/no-static-element-interactions": "off"
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"jsx-a11y/label-has-associated-control": [ "error", {
|
||||
"controlComponents": ["NumberInput"],
|
||||
"depth": 2
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
4
.github/workflows/bump-version.yml
vendored
4
.github/workflows/bump-version.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
id: regex-match
|
||||
with:
|
||||
text: ${{ github.event.inputs.version }}
|
||||
regex: '^(\d+.\d+).\d+(?:-beta.\d+)?$'
|
||||
regex: '^(\d+.\d+).\d+(?:-beta\d+)?$'
|
||||
- uses: actions-ecosystem/action-regex-match@v2.0.2
|
||||
if: ${{ inputs.version_call != '' }}
|
||||
id: regex-match-version-call
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: |
|
||||
echo "The input version format is not correct, please respect:\
|
||||
major.minor.patch or major.minor.patch-beta.number format. \
|
||||
example: 7.4.3 or 7.4.3-beta.1"
|
||||
example: 7.4.3 or 7.4.3-beta1"
|
||||
exit 1
|
||||
- name: Validate input version call
|
||||
if: ${{ inputs.version_call != '' && steps.regex-match-version-call.outputs.match == '' }}
|
||||
|
||||
30
.github/workflows/publish-technical-documentation-next.yml
vendored
Normal file
30
.github/workflows/publish-technical-documentation-next.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: "publish-technical-documentation-next"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "docs/sources/**"
|
||||
- "packages/grafana-*/**"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Checkout Grafana repo"
|
||||
uses: "actions/checkout@v3"
|
||||
|
||||
- name: "Clone website-sync Action"
|
||||
run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync"
|
||||
|
||||
- name: "Publish to website repository (next)"
|
||||
uses: "./.github/actions/website-sync"
|
||||
id: "publish-next"
|
||||
with:
|
||||
repository: "grafana/website"
|
||||
branch: "master"
|
||||
host: "github.com"
|
||||
github_pat: "${{ secrets.GH_BOT_ACCESS_TOKEN }}"
|
||||
source_folder: "docs/sources"
|
||||
target_folder: "content/docs/grafana/next"
|
||||
60
.github/workflows/publish-technical-documentation-release.yml
vendored
Normal file
60
.github/workflows/publish-technical-documentation-release.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: "publish-technical-documentation-release"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
paths:
|
||||
- "docs/sources/**"
|
||||
- "packages/grafana-*/**"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Checkout Grafana repo"
|
||||
uses: "actions/checkout@v3"
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Checkout Actions library"
|
||||
uses: "actions/checkout@v3"
|
||||
with:
|
||||
repository: "grafana/grafana-github-actions"
|
||||
path: "./actions"
|
||||
|
||||
- name: "Install Actions from library"
|
||||
run: "npm install --production --prefix ./actions"
|
||||
|
||||
- name: "Determine if there is a matching release tag"
|
||||
id: "has-matching-release-tag"
|
||||
uses: "./actions/has-matching-release-tag"
|
||||
with:
|
||||
ref_name: "${{ github.ref_name }}"
|
||||
release_tag_regexp: "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$"
|
||||
release_branch_regexp: "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.x$"
|
||||
|
||||
- name: "Determine technical documentation version"
|
||||
if: "steps.has-matching-release-tag.outputs.bool == 'true'"
|
||||
uses: "./actions/docs-target"
|
||||
id: "target"
|
||||
with:
|
||||
ref_name: "${{ github.ref_name }}"
|
||||
|
||||
- name: "Clone website-sync Action"
|
||||
if: "steps.has-matching-release-tag.outputs.bool == 'true'"
|
||||
run: "git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync"
|
||||
|
||||
- name: "Publish to website repository (release)"
|
||||
if: "steps.has-matching-release-tag.outputs.bool == 'true'"
|
||||
uses: "./.github/actions/website-sync"
|
||||
id: "publish-release"
|
||||
with:
|
||||
repository: "grafana/website"
|
||||
branch: "master"
|
||||
host: "github.com"
|
||||
github_pat: "${{ secrets.GH_BOT_ACCESS_TOKEN }}"
|
||||
source_folder: "docs/sources"
|
||||
target_folder: "content/docs/grafana/${{ steps.target.outputs.target }}"
|
||||
47
.github/workflows/publish.yml
vendored
47
.github/workflows/publish.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: publish_docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/sources/**'
|
||||
- 'packages/grafana-*/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: git clone --single-branch --no-tags --depth 1 -b master https://grafanabot:${{ secrets.GH_BOT_ACCESS_TOKEN }}@github.com/grafana/website-sync ./.github/actions/website-sync
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3.0.11
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-
|
||||
- run: yarn install --immutable
|
||||
- name: publish-to-git
|
||||
uses: ./.github/actions/website-sync
|
||||
id: publish
|
||||
with:
|
||||
repository: grafana/website
|
||||
branch: master
|
||||
host: github.com
|
||||
github_pat: '${{ secrets.GH_BOT_ACCESS_TOKEN }}'
|
||||
source_folder: docs/sources
|
||||
target_folder: content/docs/grafana/next
|
||||
allow_no_changes: 'true'
|
||||
- shell: bash
|
||||
run: |
|
||||
test -n "${{ steps.publish.outputs.commit_hash }}"
|
||||
test -n "${{ steps.publish.outputs.working_directory }}"
|
||||
235
CHANGELOG.md
235
CHANGELOG.md
@@ -1,3 +1,238 @@
|
||||
<!-- 9.3.0 START -->
|
||||
|
||||
# 9.3.0 (2022-11-30)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Alerting:** Enable interpolation for notification policies in file provisioning. [#58956](https://github.com/grafana/grafana/pull/58956), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ)
|
||||
- **Azure Monitor Logs:** Avoid warning when the response is empty. [#59211](https://github.com/grafana/grafana/pull/59211), [@andresmgot](https://github.com/andresmgot)
|
||||
- **Azure Monitor:** Add support to customized routes. [#54829](https://github.com/grafana/grafana/pull/54829), [@ms-hujia](https://github.com/ms-hujia)
|
||||
- **Canvas:** Add icon value mapping. [#59013](https://github.com/grafana/grafana/pull/59013), [@nmarrs](https://github.com/nmarrs)
|
||||
- **CloudWatch:** Cross-account querying support. [#59362](https://github.com/grafana/grafana/pull/59362), [@sunker](https://github.com/sunker)
|
||||
- **Docs:** Update `merge-pull-request.md` regarding backport policies. [#59239](https://github.com/grafana/grafana/pull/59239), [@dsotirakis](https://github.com/dsotirakis)
|
||||
- **GaugePanel:** Setting the neutral-point of a gauge. [#53989](https://github.com/grafana/grafana/pull/53989), [@sfranzis](https://github.com/sfranzis)
|
||||
- **Geomap:** Improve location editor. [#58017](https://github.com/grafana/grafana/pull/58017), [@drew08t](https://github.com/drew08t)
|
||||
- **Internationalization:** Enable internationalization by default. [#59204](https://github.com/grafana/grafana/pull/59204), [@joshhunt](https://github.com/joshhunt)
|
||||
- **Logs:** Add `Download logs` button to log log-browser. [#55163](https://github.com/grafana/grafana/pull/55163), [@svennergr](https://github.com/svennergr)
|
||||
- **Loki:** Add `gzip` compression to resource calls. [#59059](https://github.com/grafana/grafana/pull/59059), [@svennergr](https://github.com/svennergr)
|
||||
- **Loki:** Add improvements to loki label browser. [#59387](https://github.com/grafana/grafana/pull/59387), [@gwdawson](https://github.com/gwdawson)
|
||||
- **Loki:** Make label browser accessible in query builder. [#58525](https://github.com/grafana/grafana/pull/58525), [@gwdawson](https://github.com/gwdawson)
|
||||
- **Loki:** Remove raw query toggle. [#59125](https://github.com/grafana/grafana/pull/59125), [@gwdawson](https://github.com/gwdawson)
|
||||
- **Middleware:** Add CSP Report Only support. [#58074](https://github.com/grafana/grafana/pull/58074), [@jcalisto](https://github.com/jcalisto)
|
||||
- **Navigation:** Prevent viewer role accessing dashboard creation, import and folder creation. [#58842](https://github.com/grafana/grafana/pull/58842), [@lpskdl](https://github.com/lpskdl)
|
||||
- **OAuth:** Refactor OAuth parameters handling to support obtaining refresh tokens for Google OAuth. [#58782](https://github.com/grafana/grafana/pull/58782), [@mgyongyosi](https://github.com/mgyongyosi)
|
||||
- **Oauth:** Display friendly error message when role_attribute_strict=true and no valid role found. [#57818](https://github.com/grafana/grafana/pull/57818), [@kalleep](https://github.com/kalleep)
|
||||
- **Preferences:** Add confirmation modal when saving org preferences. [#59119](https://github.com/grafana/grafana/pull/59119), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana)
|
||||
- **PublicDashboards:** Orphaned public dashboard deletion script added. [#57917](https://github.com/grafana/grafana/pull/57917), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **Query Editor:** Hide overflow for long query names. [#58840](https://github.com/grafana/grafana/pull/58840), [@zuchka](https://github.com/zuchka)
|
||||
- **Reports:** Configurable timezone. (Enterprise)
|
||||
- **Solo Panel:** Configurable timezone. [#59153](https://github.com/grafana/grafana/pull/59153), [@spinillos](https://github.com/spinillos)
|
||||
- **TablePanel:** Add support for Count calculation per column or per entire dataset. [#58134](https://github.com/grafana/grafana/pull/58134), [@mdvictor](https://github.com/mdvictor)
|
||||
- **Tempo:** Send the correct start time when making a TraceQL query. [#59128](https://github.com/grafana/grafana/pull/59128), [@CrypticSignal](https://github.com/CrypticSignal)
|
||||
- **Various Panels:** Remove beta label from Bar Chart, Candlestick, Histogram, State Timeline, & Status History Panels. [#58557](https://github.com/grafana/grafana/pull/58557), [@codeincarnate](https://github.com/codeincarnate)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Access Control:** Clear user's permission cache after resource creation. [#59307](https://github.com/grafana/grafana/pull/59307), [@grafanabot](https://github.com/grafanabot)
|
||||
- **Access Control:** Clear user's permission cache after resource creation. [#59101](https://github.com/grafana/grafana/pull/59101), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
|
||||
- **Accessibility:** Improve keyboard accessibility in `AnnoListPanel`. [#58971](https://github.com/grafana/grafana/pull/58971), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Accessibility:** Improve keyboard accessibility in `Collapse`. [#59022](https://github.com/grafana/grafana/pull/59022), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Accessibility:** Improve keyboard accessibility in `GettingStarted` panel. [#58966](https://github.com/grafana/grafana/pull/58966), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Accessibility:** Improve keyboard accessibility of `FilterPill`. [#58976](https://github.com/grafana/grafana/pull/58976), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Admin:** Fix broken links to image assets in email templates. [#58729](https://github.com/grafana/grafana/pull/58729), [@zuchka](https://github.com/zuchka)
|
||||
- **Azure Monitor:** Fix namespace selection for storageaccounts. [#56449](https://github.com/grafana/grafana/pull/56449), [@andresmgot](https://github.com/andresmgot)
|
||||
- **Calcs:** Fix difference percent in legend. [#59243](https://github.com/grafana/grafana/pull/59243), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **DataLinks:** Improve Data-Links AutoComplete Logic. [#58934](https://github.com/grafana/grafana/pull/58934), [@zuchka](https://github.com/zuchka)
|
||||
- **Explore:** Fix a11y issue with logs navigation buttons. [#58944](https://github.com/grafana/grafana/pull/58944), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Heatmap:** Fix blurry text & rendering. [#59260](https://github.com/grafana/grafana/pull/59260), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Heatmap:** Fix tooltip y range of top and bottom buckets in calculated heatmaps. [#59172](https://github.com/grafana/grafana/pull/59172), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Logs:** Fix misalignment of LogRows. [#59279](https://github.com/grafana/grafana/pull/59279), [@svennergr](https://github.com/svennergr)
|
||||
- **Navigation:** Stop clearing search state when opening a result in a new tab. [#58880](https://github.com/grafana/grafana/pull/58880), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **OptionsUI:** SliderValueEditor does not get auto focused on slider change. [#59209](https://github.com/grafana/grafana/pull/59209), [@eledobleefe](https://github.com/eledobleefe)
|
||||
- **PanelEdit:** Fixes bug with not remembering panel options pane collapse/expand state. [#59265](https://github.com/grafana/grafana/pull/59265), [@torkelo](https://github.com/torkelo)
|
||||
- **Query Caching:** Skip 207 status codes. (Enterprise)
|
||||
- **Quota:** Fix failure in store due to missing scope parameters. [#58874](https://github.com/grafana/grafana/pull/58874), [@papagian](https://github.com/papagian)
|
||||
- **Quota:** Fix failure when checking session limits. [#58865](https://github.com/grafana/grafana/pull/58865), [@papagian](https://github.com/papagian)
|
||||
- **Reports:** Fix time preview. (Enterprise)
|
||||
- **StateTimeline:** Prevent label text from overflowing state rects. [#59169](https://github.com/grafana/grafana/pull/59169), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Tempo:** Fix search table duration unit. [#58642](https://github.com/grafana/grafana/pull/58642), [@joey-grafana](https://github.com/joey-grafana)
|
||||
- **TraceView:** Fix broken rendering when scrolling in Dashboard panel in Firefox. [#56642](https://github.com/grafana/grafana/pull/56642), [@zdg-github](https://github.com/zdg-github)
|
||||
|
||||
### Plugin development fixes & changes
|
||||
|
||||
- **GrafanaUI:** Add disabled option for menu items. [#58980](https://github.com/grafana/grafana/pull/58980), [@going-confetti](https://github.com/going-confetti)
|
||||
|
||||
<!-- 9.3.0 END -->
|
||||
<!-- 9.3.0-beta1 START -->
|
||||
|
||||
# 9.3.0-beta1 (2022-11-15)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Alerting:** Add Alertmanager choice warning. [#55311](https://github.com/grafana/grafana/pull/55311), [@konrad147](https://github.com/konrad147)
|
||||
- **Alerting:** Add support for linking external images securely - Azure Blob (#1). [#56598](https://github.com/grafana/grafana/pull/56598), [@petr-stupka](https://github.com/petr-stupka)
|
||||
- **Alerting:** Add threshold expression. [#55102](https://github.com/grafana/grafana/pull/55102), [@gillesdemey](https://github.com/gillesdemey)
|
||||
- **Alerting:** Add traceability headers for alert queries. [#57127](https://github.com/grafana/grafana/pull/57127), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Allow none provenance alert rule creation from provisioning API. [#58410](https://github.com/grafana/grafana/pull/58410), [@alexmobo](https://github.com/alexmobo)
|
||||
- **Alerting:** Cache result of dashboard ID lookups. [#56587](https://github.com/grafana/grafana/pull/56587), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Expressions pipeline redesign. [#54601](https://github.com/grafana/grafana/pull/54601), [@gillesdemey](https://github.com/gillesdemey)
|
||||
- **Alerting:** Fall back to "range" query type for unified alerting when "both" is specified. [#57288](https://github.com/grafana/grafana/pull/57288), [@gillesdemey](https://github.com/gillesdemey)
|
||||
- **Alerting:** Implement the Webex notifier. [#58480](https://github.com/grafana/grafana/pull/58480), [@gotjosh](https://github.com/gotjosh)
|
||||
- **Alerting:** Improve group modal with validation on evaluation interval. [#57830](https://github.com/grafana/grafana/pull/57830), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
|
||||
- **Alerting:** Persist annotations from multidimensional rules in batches. [#56575](https://github.com/grafana/grafana/pull/56575), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Query time logging. [#57585](https://github.com/grafana/grafana/pull/57585), [@konrad147](https://github.com/konrad147)
|
||||
- **Alerting:** Remove the alert manager selection from the data source configuration. [#57369](https://github.com/grafana/grafana/pull/57369), [@VikaCep](https://github.com/VikaCep)
|
||||
- **Alerting:** Remove the alert manager selection from the data source configuration. [#56460](https://github.com/grafana/grafana/pull/56460), [@gitstart](https://github.com/gitstart)
|
||||
- **Alerting:** Support values in notification templates. [#56457](https://github.com/grafana/grafana/pull/56457), [@grobinson-grafana](https://github.com/grobinson-grafana)
|
||||
- **Alerting:** Templated URLs for webhook type contact points. [#57296](https://github.com/grafana/grafana/pull/57296), [@santihernandezc](https://github.com/santihernandezc)
|
||||
- **Annotations:** Disable "Add annotation" button when annotations are disabled. [#57481](https://github.com/grafana/grafana/pull/57481), [@ryantxu](https://github.com/ryantxu)
|
||||
- **Auth:** Add validation and ingestion of conflict file. [#53014](https://github.com/grafana/grafana/pull/53014), [@eleijonmarck](https://github.com/eleijonmarck)
|
||||
- **Auth:** Make built-in login configurable. [#46978](https://github.com/grafana/grafana/pull/46978), [@TsotosA](https://github.com/TsotosA)
|
||||
- **Auth:** Refresh OAuth access_token automatically using the refresh_token. [#56076](https://github.com/grafana/grafana/pull/56076), [@mgyongyosi](https://github.com/mgyongyosi)
|
||||
- **Auth:** Validate Azure ID token version on login is not v1. [#58088](https://github.com/grafana/grafana/pull/58088), [@Jguer](https://github.com/Jguer)
|
||||
- **BackendSrv:** Make it possible to pass `options` to `.get|post|patch...` methods. [#51316](https://github.com/grafana/grafana/pull/51316), [@leventebalogh](https://github.com/leventebalogh)
|
||||
- **Canvas:** Add tabs to inline editor. [#57778](https://github.com/grafana/grafana/pull/57778), [@adela-almasan](https://github.com/adela-almasan)
|
||||
- **Canvas:** Extend root context menu. [#58097](https://github.com/grafana/grafana/pull/58097), [@adela-almasan](https://github.com/adela-almasan)
|
||||
- **Chore:** Switch Grafana to using faro libraries. [#58186](https://github.com/grafana/grafana/pull/58186), [@tolzhabayev](https://github.com/tolzhabayev)
|
||||
- **Chore:** Use strings.ReplaceAll and preallocate containers. [#58483](https://github.com/grafana/grafana/pull/58483), [@sashamelentyev](https://github.com/sashamelentyev)
|
||||
- **CloudWatch:** Cache resource request responses in the browser. [#57082](https://github.com/grafana/grafana/pull/57082), [@sunker](https://github.com/sunker)
|
||||
- **Config:** Change jwt config value to be "expect_claims". [#58284](https://github.com/grafana/grafana/pull/58284), [@conorevans](https://github.com/conorevans)
|
||||
- **Configuration:** Update ssl_mode documentation in sample.ini to match default.ini. [#55138](https://github.com/grafana/grafana/pull/55138), [@alecxvs](https://github.com/alecxvs)
|
||||
- **Correlations:** Add query editor and target field to settings page. [#55567](https://github.com/grafana/grafana/pull/55567), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Dashboard:** Record the number of cached queries for usage insights. [#56050](https://github.com/grafana/grafana/pull/56050), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **Dashboard:** Record the number of cached queries for usage insights. (Enterprise)
|
||||
- **Datasources:** Support mixed datasources in a single query. [#56832](https://github.com/grafana/grafana/pull/56832), [@mmandrus](https://github.com/mmandrus)
|
||||
- **Docs:** Add documentation for Custom Branding on Public Dashboards. [#58090](https://github.com/grafana/grafana/pull/58090), [@leandro-deveikis](https://github.com/leandro-deveikis)
|
||||
- **Docs:** Add missing documentation for enterprise features. [#56753](https://github.com/grafana/grafana/pull/56753), [@mmandrus](https://github.com/mmandrus)
|
||||
- **Docs:** Clarify that audit logs are generated only for API requests. [#57521](https://github.com/grafana/grafana/pull/57521), [@spinillos](https://github.com/spinillos)
|
||||
- **Echo:** Add config option to prevent duplicate page views for GA4. [#57619](https://github.com/grafana/grafana/pull/57619), [@tolzhabayev](https://github.com/tolzhabayev)
|
||||
- **Elasticsearch:** Add trace to logs functionality. [#58063](https://github.com/grafana/grafana/pull/58063), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Elasticsearch:** Reuse http client in the backend. [#55172](https://github.com/grafana/grafana/pull/55172), [@gabor](https://github.com/gabor)
|
||||
- **Explore:** Add tracesToMetrics span time shift options (#54710). [#55335](https://github.com/grafana/grafana/pull/55335), [@hanjm](https://github.com/hanjm)
|
||||
- **Explore:** Logs volume histogram: always start Y axis from zero. [#56200](https://github.com/grafana/grafana/pull/56200), [@gabor](https://github.com/gabor)
|
||||
- **Explore:** Remove explore2Dashboard feature toggle. [#58329](https://github.com/grafana/grafana/pull/58329), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Explore:** Support fields interpolation in logs panel. [#58426](https://github.com/grafana/grafana/pull/58426), [@ifrost](https://github.com/ifrost)
|
||||
- **Frontend Routing:** Always render standalone plugin pages using the `<AppRootPage>`. [#57771](https://github.com/grafana/grafana/pull/57771), [@leventebalogh](https://github.com/leventebalogh)
|
||||
- **GRPC Server:** Add gRPC server service. [#47849](https://github.com/grafana/grafana/pull/47849), [@FZambia](https://github.com/FZambia)
|
||||
- **Geomap:** Add photo layer. [#57307](https://github.com/grafana/grafana/pull/57307), [@drew08t](https://github.com/drew08t)
|
||||
- **Geomap:** Upgrade to openlayers 7.x. [#57317](https://github.com/grafana/grafana/pull/57317), [@ryantxu](https://github.com/ryantxu)
|
||||
- **GrafanaData:** Deprecate logs functions. [#56077](https://github.com/grafana/grafana/pull/56077), [@gabor](https://github.com/gabor)
|
||||
- **GrafanaData:** Deprecate the LogsParser type. [#56242](https://github.com/grafana/grafana/pull/56242), [@gabor](https://github.com/gabor)
|
||||
- **Kindsys:** Introduce Kind framework. [#56492](https://github.com/grafana/grafana/pull/56492), [@sdboyer](https://github.com/sdboyer)
|
||||
- **LDAP:** Add `skip_org_role_sync` configuration option. [#56792](https://github.com/grafana/grafana/pull/56792), [@grafanabot](https://github.com/grafanabot)
|
||||
- **LDAP:** Add `skip_org_role_sync` configuration option. [#56679](https://github.com/grafana/grafana/pull/56679), [@gamab](https://github.com/gamab)
|
||||
- **LDAPSync:** Improve performance of sync and make it case insensitive. (Enterprise)
|
||||
- **LibraryPanels:** Load library panels in the frontend rather than the backend. [#50560](https://github.com/grafana/grafana/pull/50560), [@ryantxu](https://github.com/ryantxu)
|
||||
- **LogContext:** Add header and close button to modal. [#56283](https://github.com/grafana/grafana/pull/56283), [@svennergr](https://github.com/svennergr)
|
||||
- **LogContext:** Improve text describing the loglines. [#55475](https://github.com/grafana/grafana/pull/55475), [@svennergr](https://github.com/svennergr)
|
||||
- **Logs:** Allow collapsing the logs volume histogram. [#52808](https://github.com/grafana/grafana/pull/52808), [@gabor](https://github.com/gabor)
|
||||
- **Logs:** Center `show context` modal on click. [#55989](https://github.com/grafana/grafana/pull/55989), [@svennergr](https://github.com/svennergr)
|
||||
- **Logs:** Center `show context` modal on click. [#55405](https://github.com/grafana/grafana/pull/55405), [@svennergr](https://github.com/svennergr)
|
||||
- **Logs:** Show LogRowMenu also for long logs and wrap-lines turned off. [#56030](https://github.com/grafana/grafana/pull/56030), [@svennergr](https://github.com/svennergr)
|
||||
- **LogsContext:** Added button to load 10 more log lines. [#55923](https://github.com/grafana/grafana/pull/55923), [@svennergr](https://github.com/svennergr)
|
||||
- **Loki:** Add case insensitive line contains operation. [#58177](https://github.com/grafana/grafana/pull/58177), [@gwdawson](https://github.com/gwdawson)
|
||||
- **Loki:** Monaco Query Editor enabled by default. [#58080](https://github.com/grafana/grafana/pull/58080), [@matyax](https://github.com/matyax)
|
||||
- **Loki:** Redesign and improve query patterns. [#55097](https://github.com/grafana/grafana/pull/55097), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Loki:** Rename log browser to label browser. [#58416](https://github.com/grafana/grafana/pull/58416), [@gwdawson](https://github.com/gwdawson)
|
||||
- **Loki:** Show invalid fields in label filter. [#55751](https://github.com/grafana/grafana/pull/55751), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **MSSQL:** Add connection timeout setting in configuration page. [#58631](https://github.com/grafana/grafana/pull/58631), [@mdvictor](https://github.com/mdvictor)
|
||||
- **Navigation:** Add `pluginId` to standalone plugin page NavLinks. [#57769](https://github.com/grafana/grafana/pull/57769), [@leventebalogh](https://github.com/leventebalogh)
|
||||
- **Navigation:** Expose new props to extend `Page`/`PluginPage`. [#58465](https://github.com/grafana/grafana/pull/58465), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Navtree:** Make it possible to configure standalone plugin pages. [#56393](https://github.com/grafana/grafana/pull/56393), [@leventebalogh](https://github.com/leventebalogh)
|
||||
- **Node Graph:** Always show context menu. [#56876](https://github.com/grafana/grafana/pull/56876), [@joey-grafana](https://github.com/joey-grafana)
|
||||
- **Number formatting:** Strip trailing zeros after decimal point when decimals=auto. [#57373](https://github.com/grafana/grafana/pull/57373), [@leeoniya](https://github.com/leeoniya)
|
||||
- **OAuth:** Feature toggle for access token expiration check and docs. [#58179](https://github.com/grafana/grafana/pull/58179), [@mgyongyosi](https://github.com/mgyongyosi)
|
||||
- **Opentsdb:** Allow template variables for filter keys. [#57226](https://github.com/grafana/grafana/pull/57226), [@bohandley](https://github.com/bohandley)
|
||||
- **PanelEdit:** Allow test id to be passed to panel editors. [#55417](https://github.com/grafana/grafana/pull/55417), [@mckn](https://github.com/mckn)
|
||||
- **Plugins:** Add hook to make it easier to track interactions in plugins. [#56126](https://github.com/grafana/grafana/pull/56126), [@mckn](https://github.com/mckn)
|
||||
- **Plugins:** Introduce new Flame graph panel. [#56376](https://github.com/grafana/grafana/pull/56376), [@joey-grafana](https://github.com/joey-grafana)
|
||||
- **Plugins:** Make "README" the default markdown request param. [#58264](https://github.com/grafana/grafana/pull/58264), [@wbrowne](https://github.com/wbrowne)
|
||||
- **PostgreSQL:** Migrate to React. [#52831](https://github.com/grafana/grafana/pull/52831), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **Preferences:** Create indices. [#48356](https://github.com/grafana/grafana/pull/48356), [@sakjur](https://github.com/sakjur)
|
||||
- **Profiling:** Add Phlare and Parca datasources. [#57809](https://github.com/grafana/grafana/pull/57809), [@aocenas](https://github.com/aocenas)
|
||||
- **Prometheus:** Handle errors and warnings in buffered client. [#58504](https://github.com/grafana/grafana/pull/58504), [@itsmylife](https://github.com/itsmylife)
|
||||
- **Prometheus:** Make Prometheus streaming parser as default client. [#58365](https://github.com/grafana/grafana/pull/58365), [@itsmylife](https://github.com/itsmylife)
|
||||
- **Public Dashboards:** Add audit table. [#54508](https://github.com/grafana/grafana/pull/54508), [@jalevin](https://github.com/jalevin)
|
||||
- **PublicDashboards:** Add PubDash support to Angular panel plugins. [#57293](https://github.com/grafana/grafana/pull/57293), [@mmandrus](https://github.com/mmandrus)
|
||||
- **PublicDashboards:** Add annotations support. [#56413](https://github.com/grafana/grafana/pull/56413), [@owensmallwood](https://github.com/owensmallwood)
|
||||
- **PublicDashboards:** Add custom branding for Public Dashboard. (Enterprise)
|
||||
- **PublicDashboards:** Add delete public dashboard button in public dashboard modal. [#58095](https://github.com/grafana/grafana/pull/58095), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **PublicDashboards:** Cached queries column added in public dashboard insight query. (Enterprise)
|
||||
- **PublicDashboards:** Can toggle annotations in modal. [#57312](https://github.com/grafana/grafana/pull/57312), [@owensmallwood](https://github.com/owensmallwood)
|
||||
- **PublicDashboards:** Delete public dashboard in public dashboard table. [#57766](https://github.com/grafana/grafana/pull/57766), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **PublicDashboards:** Delete public dashboard when dashboard is deleted. [#57291](https://github.com/grafana/grafana/pull/57291), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **PublicDashboards:** Extract config of Public Dashboard. [#57788](https://github.com/grafana/grafana/pull/57788), [@leandro-deveikis](https://github.com/leandro-deveikis)
|
||||
- **PublicDashboards:** Hide top navigation bar. [#56873](https://github.com/grafana/grafana/pull/56873), [@evictorero](https://github.com/evictorero)
|
||||
- **PublicDashboards:** Make mixed datasource calls concurrently. [#56421](https://github.com/grafana/grafana/pull/56421), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **PublicDashboards:** Orphaned public dashboard item list modified. [#58014](https://github.com/grafana/grafana/pull/58014), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **PublicDashboards:** Rename PubdashFooter frontend component. [#58137](https://github.com/grafana/grafana/pull/58137), [@leandro-deveikis](https://github.com/leandro-deveikis)
|
||||
- **PublicDashboards:** Update docs with supported datasources. [#57629](https://github.com/grafana/grafana/pull/57629), [@owensmallwood](https://github.com/owensmallwood)
|
||||
- **PublicDashboards:** Validate access token. [#57298](https://github.com/grafana/grafana/pull/57298), [@leandro-deveikis](https://github.com/leandro-deveikis)
|
||||
- **PublicDashboards:** Validate access token not to be duplicated and add retries. [#56755](https://github.com/grafana/grafana/pull/56755), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **RBAC:** Improve performance of dashboard filter query. [#56813](https://github.com/grafana/grafana/pull/56813), [@kalleep](https://github.com/kalleep)
|
||||
- **Rendering:** Add configuration options for `renderKey` lifetime. [#57339](https://github.com/grafana/grafana/pull/57339), [@Willena](https://github.com/Willena)
|
||||
- **Reports:** Dynamic scale factor per report. (Enterprise)
|
||||
- **SAML:** Set cookie option SameSite=none and Secure=true. (Enterprise)
|
||||
- **SQLStore:** Optionally retry queries if sqlite returns database is locked. [#56096](https://github.com/grafana/grafana/pull/56096), [@papagian](https://github.com/papagian)
|
||||
- **Server:** Make unix socket permission configurable. [#52944](https://github.com/grafana/grafana/pull/52944), [@unknowndevQwQ](https://github.com/unknowndevQwQ)
|
||||
- **Tempo:** Add start time and end time parameters while querying traces. [#48068](https://github.com/grafana/grafana/pull/48068), [@bikashmishra100](https://github.com/bikashmishra100)
|
||||
- **TimeSeries:** Render null-bounded points at data edges. [#57798](https://github.com/grafana/grafana/pull/57798), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Tracing:** Allow trace to logs for OpenSearch. [#58161](https://github.com/grafana/grafana/pull/58161), [@gabor](https://github.com/gabor)
|
||||
- **Transformers:** PartitionByValues. [#56767](https://github.com/grafana/grafana/pull/56767), [@leeoniya](https://github.com/leeoniya)
|
||||
- **UsageStats:** Add traces when sending usage stats. [#55474](https://github.com/grafana/grafana/pull/55474), [@sakjur](https://github.com/sakjur)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Alerting:** Fix mathexp.NoData in ConditionsCmd. [#56812](https://github.com/grafana/grafana/pull/56812), [@grobinson-grafana](https://github.com/grobinson-grafana)
|
||||
- **BarChart:** Fix coloring from thresholds and value mappings. [#58285](https://github.com/grafana/grafana/pull/58285), [@leeoniya](https://github.com/leeoniya)
|
||||
- **BarChart:** Fix stacked hover. [#57711](https://github.com/grafana/grafana/pull/57711), [@leeoniya](https://github.com/leeoniya)
|
||||
- **Explore:** Fix shared crosshair for logs, logsvolume and graph panels. [#57892](https://github.com/grafana/grafana/pull/57892), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Flame Graph:** Exact search. [#56769](https://github.com/grafana/grafana/pull/56769), [@joey-grafana](https://github.com/joey-grafana)
|
||||
- **Flame Graph:** Fix for dashboard scrolling. [#56555](https://github.com/grafana/grafana/pull/56555), [@joey-grafana](https://github.com/joey-grafana)
|
||||
- **LogContext:** Fix scroll behavior in context modal. [#56070](https://github.com/grafana/grafana/pull/56070), [@svennergr](https://github.com/svennergr)
|
||||
- **Loki:** Fix showing of history of querying in query editor. [#57344](https://github.com/grafana/grafana/pull/57344), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **OAuth:** Fix misleading warn log related to oauth and increase logged content. [#57336](https://github.com/grafana/grafana/pull/57336), [@Jguer](https://github.com/Jguer)
|
||||
- **Plugins:** Plugin details page visual alignment issues. [#57729](https://github.com/grafana/grafana/issues/57729)
|
||||
- **PublicDashboards:** Fix GET public dashboard that doesn't match. [#57571](https://github.com/grafana/grafana/pull/57571), [@juanicabanas](https://github.com/juanicabanas)
|
||||
- **PublicDashboards:** Fix annotations error for public dashboards. [#57455](https://github.com/grafana/grafana/pull/57455), [@leandro-deveikis](https://github.com/leandro-deveikis)
|
||||
- **PublicDashboards:** Fix granularity discrepancy between public and original dashboard. [#57129](https://github.com/grafana/grafana/pull/57129), [@guicaulada](https://github.com/guicaulada)
|
||||
- **PublicDashboards:** Fix granularity issue caused by query caching. (Enterprise)
|
||||
- **PublicDashboards:** Fix hidden queries execution. (Enterprise)
|
||||
- **RBAC:** Add primary key to seed_assignment table. [#56540](https://github.com/grafana/grafana/pull/56540), [@kalleep](https://github.com/kalleep)
|
||||
- **Tempo:** Fix search removing service name from query. [#58630](https://github.com/grafana/grafana/pull/58630), [@joey-grafana](https://github.com/joey-grafana)
|
||||
- **TimeRangeInput:** Fix clear button type. [#56545](https://github.com/grafana/grafana/pull/56545), [@Clarity-89](https://github.com/Clarity-89)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Removes the unused close-milestone command from `@grafana/toolkit`. Issue [#57062](https://github.com/grafana/grafana/issues/57062)
|
||||
|
||||
@grafana/toolkit `cherrypick` command was removed. Issue [#56114](https://github.com/grafana/grafana/issues/56114)
|
||||
|
||||
`EmotionPerfTest` is no longer exported from the `@grafana/ui` bundle. Issue [#56100](https://github.com/grafana/grafana/issues/56100)
|
||||
|
||||
Removing the unused `changelog` command in `@grafana/toolkit`. Issue [#56073](https://github.com/grafana/grafana/issues/56073)
|
||||
|
||||
### Deprecations
|
||||
|
||||
The interface type `LogsParser` in `grafana-data` is deprecated. Issue [#56242](https://github.com/grafana/grafana/issues/56242)
|
||||
|
||||
The following functions and classes related to logs are deprecated in the `grafana-ui` package: `getLogLevel`, `getLogLevelFromKey`, `addLogLevelToSeries`, `LogsParsers`, `calculateFieldStats`, `calculateLogsLabelStats`, `calculateStats`, `getParser`, `sortInAscendingOrder`, `sortInDescendingOrder`, `sortLogsResult`, `sortLogRows`, `checkLogsError`, `escapeUnescapedString`. Issue [#56077](https://github.com/grafana/grafana/issues/56077)
|
||||
|
||||
### Plugin development fixes & changes
|
||||
|
||||
- **Toolkit:** Deprecate `plugin:update-circleci` command. [#57743](https://github.com/grafana/grafana/pull/57743), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Deprecate `plugin:github-publish` command. [#57726](https://github.com/grafana/grafana/pull/57726), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Deprecate `plugin:bundle-managed` command and move its functionality to a bash script. [#57719](https://github.com/grafana/grafana/pull/57719), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Deprecate and replace toolkit:build with plain yarn scripts. [#57620](https://github.com/grafana/grafana/pull/57620), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Deprecate node-version-check command. [#57591](https://github.com/grafana/grafana/pull/57591), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Deprecate searchTestData command. [#57589](https://github.com/grafana/grafana/pull/57589), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Remove unused close-milestone command. [#57062](https://github.com/grafana/grafana/pull/57062), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Remove unused legacy cherrypick command. [#56114](https://github.com/grafana/grafana/pull/56114), [@academo](https://github.com/academo)
|
||||
- **Grafana UI:** Clean up bundle. [#56100](https://github.com/grafana/grafana/pull/56100), [@jackw](https://github.com/jackw)
|
||||
- **Toolkit:** Deprecate `component:create` command. [#56086](https://github.com/grafana/grafana/pull/56086), [@academo](https://github.com/academo)
|
||||
- **Toolkit:** Remove changelog command. [#56073](https://github.com/grafana/grafana/pull/56073), [@gitstart](https://github.com/gitstart)
|
||||
|
||||
<!-- 9.3.0-beta1 END -->
|
||||
<!-- 9.2.4 START -->
|
||||
|
||||
# 9.2.4 (2022-11-07)
|
||||
|
||||
@@ -1256,6 +1256,8 @@ license_path =
|
||||
# enable = feature1,feature2
|
||||
enable =
|
||||
|
||||
internationalization = true
|
||||
|
||||
# feature1 = true
|
||||
# feature2 = false
|
||||
|
||||
|
||||
@@ -27,9 +27,6 @@ Before you can create your first dashboard, you need to add your data source.
|
||||
**To add a data source:**
|
||||
|
||||
1. Select the cog icon on the side menu to show the configuration options.
|
||||
|
||||
{{< figure src="/static/img/docs/v75/sidemenu-datasource-7-5.png" max-width="150px" class="docs-image--no-shadow">}}
|
||||
|
||||
1. Select **Data sources**.
|
||||
|
||||
This opens the data sources page, which displays a list of previously configured data sources for the Grafana instance.
|
||||
@@ -44,9 +41,7 @@ Before you can create your first dashboard, you need to add your data source.
|
||||
|
||||
1. Move the cursor over the data source you want to add.
|
||||
|
||||
{{< figure src="/static/img/docs/v75/select-data-source-7-5.png" max-width="700px" class="docs-image--no-shadow">}}
|
||||
|
||||
1. Select **Select**.
|
||||
1. Click **Select**.
|
||||
|
||||
This opens the data source configuration page.
|
||||
|
||||
@@ -63,8 +58,6 @@ Each data source's configuration includes a permissions page where you can enabl
|
||||
|
||||
### Enable data source permissions
|
||||
|
||||
{{< figure src="/static/img/docs/enterprise/datasource_permissions_enable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/static/img/docs/enterprise/datasource_permissions_enable.gif" >}}
|
||||
|
||||
By default, data sources in an organization can be queried by any user in that organization. For example, a user with the `Viewer` role can issue any possible query to a data source, not just
|
||||
queries that exist on dashboards they have access to.
|
||||
|
||||
@@ -82,8 +75,6 @@ When permissions are enabled for a data source in an organization, the user who
|
||||
|
||||
### Allow users and teams to query a data source
|
||||
|
||||
{{< figure src="/static/img/docs/enterprise/datasource_permissions_add_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/static/img/docs/enterprise/datasource_permissions_add.gif" >}}
|
||||
|
||||
After you have enabled permissions for a data source you can assign query permissions to users and teams which will allow access to query the data source.
|
||||
|
||||
**Assign query permission to users and teams:**
|
||||
@@ -98,8 +89,6 @@ After you have enabled permissions for a data source you can assign query permis
|
||||
|
||||
### Disable data source permissions
|
||||
|
||||
{{< figure src="/static/img/docs/enterprise/datasource_permissions_disable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/static/img/docs/enterprise/datasource_permissions_disable.gif" >}}
|
||||
|
||||
If you have enabled permissions for a data source and want to return data source permissions to the default, then you can disable permissions with a click of a button.
|
||||
|
||||
Note that _all_ existing permissions created for the data source will be deleted.
|
||||
|
||||
@@ -60,8 +60,6 @@ Complete this task when you want to view a list of existing organizations.
|
||||
|
||||
A list of organizations appears.
|
||||
|
||||

|
||||
|
||||
## Create an organization
|
||||
|
||||
Create an organization when you want to isolate dashboards and other resources from each other.
|
||||
@@ -117,5 +115,3 @@ Edit an organization when you want to change its name.
|
||||
1. Hover your cursor over the **Server Admin** (shield) icon until a menu appears, and click **Orgs**.
|
||||
1. Click the organization you want to edit.
|
||||
1. Update the organization name and click **Update**.
|
||||
|
||||

|
||||
|
||||
@@ -101,7 +101,7 @@ Here is an example of the light theme.
|
||||
|
||||
### Change server UI theme
|
||||
|
||||
Grafana server administrators can change the Grafana UI theme for all users on the server by setting the [default_theme]({{< relref "../../setup-grafana/configure-grafana/#default-theme" >}}) option in the Grafana configuration file.
|
||||
As a Grafana server administrator, you can change the default Grafana UI theme for all users who are on the server by setting the [default_theme]({{< relref "../../setup-grafana/configure-grafana/#default-theme" >}}) option in the Grafana configuration file.
|
||||
|
||||
To see what the current settings are, refer to [View server settings]({{< relref "../stats-and-license#view-server-settings" >}}).
|
||||
|
||||
@@ -111,17 +111,18 @@ Organization administrators can change the UI theme for all users in an organiza
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon.
|
||||
1. Click **Preferences**.
|
||||
1. In the Preferences section, select the **UI theme**.
|
||||
1. In the **Preferences** section, select the **UI theme**.
|
||||
1. Click **Save**.
|
||||
|
||||
### Change team UI theme
|
||||
|
||||
Organization and team administrators can change the UI theme for all users in a team.
|
||||
Organization and team administrators can change the UI theme for all users on a team.
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu.
|
||||
1. Click **Teams**. Grafana displays the team list.
|
||||
1. Click on the team that you want to change the UI theme for and then navigate to the **Settings** tab.
|
||||
1. In the Preferences section, select the **UI theme**.
|
||||
1. Click the team for which you want to change the UI theme.
|
||||
1. Click **Settings**.
|
||||
1. In the **Preferences** section, select the **UI theme**.
|
||||
1. Click **Save**.
|
||||
|
||||
### Change your personal UI theme
|
||||
@@ -129,7 +130,7 @@ Organization and team administrators can change the UI theme for all users in a
|
||||
You can change the UI theme for your user account. This setting overrides UI theme settings at higher levels.
|
||||
|
||||
1. On the left menu, hover your cursor over your avatar and then click **Preferences**.
|
||||
1. In the Preferences section, select the **UI theme**.
|
||||
1. In the **Preferences** section, select the **UI theme**.
|
||||
1. Click **Save**.
|
||||
|
||||
## Change the Grafana default timezone
|
||||
@@ -153,11 +154,12 @@ Organization administrators can choose a default timezone for their organization
|
||||
|
||||
### Set team timezone
|
||||
|
||||
Organization administrators and team administrators can choose a default timezone for all users in a team.
|
||||
Organization administrators and team administrators can choose a default timezone for all users on a team.
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu.
|
||||
1. Click **Teams**. Grafana displays the team list.
|
||||
1. Click on the team you that you want to change the timezone for and then navigate to the **Settings** tab.
|
||||
1. Click the team for which you want to change the timezone.
|
||||
1. Click **Settings**
|
||||
1. Click to select an option in the **Timezone** list. **Default** is either the browser local timezone or the timezone selected at a higher level. Refer to [[Time range controls]({{< relref "../../dashboards/manage-dashboards/#configure-dashboard-time-range-controls" >}}) for more information about Grafana time settings.
|
||||
1. Click **Save**.
|
||||
|
||||
@@ -207,7 +209,7 @@ default_home_dashboard_path = data/main-dashboard.json
|
||||
|
||||
### Set the home dashboard for your organization
|
||||
|
||||
Organization administrators can choose a home dashboard for their organization.
|
||||
Organization administrators can choose a default home dashboard for their organization.
|
||||
|
||||
1. Navigate to the dashboard you want to set as the home dashboard.
|
||||
1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already.
|
||||
@@ -218,13 +220,14 @@ Organization administrators can choose a home dashboard for their organization.
|
||||
|
||||
### Set home dashboard for your team
|
||||
|
||||
Organization administrators and Team Admins can choose a home dashboard for a team.
|
||||
Organization administrators and Team Admins can set a default home dashboard for all users on a team.
|
||||
|
||||
1. Navigate to the dashboard you want to set as the home dashboard.
|
||||
1. Click the star next to the dashboard title to mark the dashboard as a favorite if it is not already.
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu.
|
||||
1. Click **Teams**. Grafana displays the team list.
|
||||
1. Click on the team that you want to change the home dashboard for and then navigate to the **Settings** tab.
|
||||
1. Click the team for which you want to change the home dashboard.
|
||||
1. Click **Settings**.
|
||||
1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards.
|
||||
1. Click **Save**.
|
||||
|
||||
@@ -237,3 +240,37 @@ You can choose your own personal home dashboard. This setting overrides all home
|
||||
1. On the left menu, hover your cursor over your avatar and then click **Preferences**.
|
||||
1. In the **Home Dashboard** field, select the dashboard that you want to use for your home dashboard. Options include all starred dashboards.
|
||||
1. Click **Save**.
|
||||
|
||||
## Change Grafana language
|
||||
|
||||
### Change server language
|
||||
|
||||
Grafana server administrators can change the default Grafana UI language for all users on the server by setting the [default_language]({{< relref "../../setup-grafana/configure-grafana/#default-language" >}}) option in the Grafana configuration file.
|
||||
|
||||
### Change organization language
|
||||
|
||||
Organization administrators can change the language for all users in an organization.
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon.
|
||||
1. Click **Preferences**.
|
||||
1. In the **Preferences** section, select the **Language**.
|
||||
1. Click **Save**.
|
||||
|
||||
### Change team language
|
||||
|
||||
Organization and team administrators can set a default language for all users on a team.
|
||||
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu.
|
||||
1. Click **Teams**. Grafana displays the team list.
|
||||
1. Click the team for which you want to change the language.
|
||||
1. Click **Settings**
|
||||
1. In the **Preferences** section, select the **Language**.
|
||||
1. Click **Save**.
|
||||
|
||||
### Change your personal language
|
||||
|
||||
You can change the language for your user account. This setting overrides language settings at higher levels.
|
||||
|
||||
1. On the left menu, hover your cursor over your avatar and then click **Preferences**.
|
||||
1. In the **Preferences** section, select the **language**.
|
||||
1. Click **Save**.
|
||||
|
||||
@@ -88,8 +88,6 @@ To browse for available plugins:
|
||||
1. Click the **All** filter to browse all available plugins.
|
||||
1. Click the **Data sources**, **Panels**, or **Applications** buttons to filter by plugin type.
|
||||
|
||||

|
||||
|
||||
### Install a plugin
|
||||
|
||||
To install a plugin:
|
||||
@@ -101,8 +99,6 @@ To install a plugin:
|
||||
|
||||
When the update is complete, you see a confirmation message that the installation was successful.
|
||||
|
||||

|
||||
|
||||
### Update a plugin
|
||||
|
||||
To update a plugin:
|
||||
@@ -113,8 +109,6 @@ To update a plugin:
|
||||
|
||||
When the update is complete, you see a confirmation message that the update was successful.
|
||||
|
||||

|
||||
|
||||
### Uninstall a plugin
|
||||
|
||||
To uninstall a plugin:
|
||||
@@ -125,8 +119,6 @@ To uninstall a plugin:
|
||||
|
||||
When the update is complete, you see a confirmation message that the uninstall was successful.
|
||||
|
||||

|
||||
|
||||
## Install Grafana plugins
|
||||
|
||||
Grafana supports data source, panel, and app plugins. Having panels as plugins makes it easy to create and add any kind of panel, to show your data, or improve your favorite dashboards. Apps enable the bundling of data sources, panels, dashboards, and Grafana pages into a cohesive experience.
|
||||
|
||||
@@ -53,8 +53,6 @@ In both cases, the assignment applies only to the user, team or service account
|
||||
4. In the **Role** column, select the fixed role that you want to assign to the user, team or service account.
|
||||
5. Click **Update**.
|
||||
|
||||

|
||||
|
||||
**To assign a fixed role as a server administrator:**
|
||||
|
||||
1. Sign in to Grafana, hover your cursor over **Server Admin** (the shield icon) in the left navigation menu, and click **Users**.
|
||||
@@ -62,8 +60,6 @@ In both cases, the assignment applies only to the user, team or service account
|
||||
1. In the **Organizations** section, select a role within an organization that you want to assign to the user.
|
||||
1. Click **Update**.
|
||||
|
||||

|
||||
|
||||
## Assign fixed or custom roles to a team using provisioning
|
||||
|
||||
Instead of using the Grafana role picker, you can use file-based provisioning to assign fixed roles to teams. If you have a large number of teams, provisioning can provide an easier approach to assigning and managing role assignments.
|
||||
|
||||
@@ -14,9 +14,10 @@ weight: 30
|
||||
|
||||
The table below describes all RBAC configuration options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../../../setup-grafana/configure-grafana/#configure-with-environment-variables" >}}).
|
||||
|
||||
| Setting | Required | Description | Default |
|
||||
| ------------------ | -------- | ---------------------------------------------------------------------------- | ------- |
|
||||
| `permission_cache` | No | Enable to use in memory cache for loading and evaluating users' permissions. | `true` |
|
||||
| Setting | Required | Description | Default |
|
||||
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `permission_cache` | No | Enable to use in memory cache for loading and evaluating users' permissions. | `true` |
|
||||
| `permission_validation_enabled` | No | Grafana enforces validation for permissions when a user creates or updates a role. The system checks the internal list of scopes and actions for each permission to determine they are valid. By default, if a scope or action is not recognized, Grafana logs a warning message. When set to `true`, Grafana returns an error. | `false` |
|
||||
|
||||
## Example RBAC configuration
|
||||
|
||||
|
||||
@@ -62,8 +62,6 @@ To add a team member:
|
||||
1. Choose if you want to add the user as a team Member or an Admin.
|
||||
1. Click **Add to team**.
|
||||
|
||||

|
||||
|
||||
## Grant team member permissions
|
||||
|
||||
Complete this task when you want to add or modify team member permissions.
|
||||
@@ -76,8 +74,6 @@ To grant team member permissions:
|
||||
1. In the team member list, find and click the user that you want to change. You can use the search field to filter the list if necessary.
|
||||
1. Click the **Permission** list, and then click the new user permission level.
|
||||
|
||||

|
||||
|
||||
## Remove a team member
|
||||
|
||||
You can remove a team member when you no longer want to apply team permissions to the user
|
||||
|
||||
@@ -33,8 +33,6 @@ You can see a list of users with accounts in your Grafana organization. If neces
|
||||
1. Sign in to Grafana as an organization administrator.
|
||||
1. Hover your cursor over the **Configuration** (gear) icon in the side menu and click **Users**.
|
||||
|
||||

|
||||
|
||||
> **Note:** If you have [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can also [view a global list of users]({{< relref "../server-user-management#view-a-list-of-users" >}}) in the Server Admin section of Grafana.
|
||||
|
||||
## Change a user's organization permissions
|
||||
@@ -96,8 +94,6 @@ When you invite users to join an organization, you assign the **Admin**, **Edito
|
||||
|
||||
If the invitee is not already a user, the system adds them.
|
||||
|
||||
.
|
||||
|
||||
## Manage a pending invitation
|
||||
|
||||
Periodically review invitations you have sent so that you can see a list of users that have not yet accepted the invitation or cancel a pending invitation.
|
||||
@@ -116,14 +112,10 @@ Periodically review invitations you have sent so that you can see a list of user
|
||||
|
||||
The **Pending Invites** button appears only when there are unaccepted invitations.
|
||||
|
||||

|
||||
|
||||
To cancel an invitation, click the red **X** next to the invitation.
|
||||
|
||||
To copy an invitation link and send it directly to a user, click Copy Invite. You can then paste the invite link into a message.
|
||||
|
||||

|
||||
|
||||
## Remove a user from an organization
|
||||
|
||||
You can remove a user from an organization when they no longer require access to the dashboard or data sources owned by the organization. No longer requiring access to an organization might occur when the user has left your company or has internally moved to another organization.
|
||||
|
||||
@@ -38,8 +38,6 @@ You can see a list of users with accounts on your Grafana server. This action mi
|
||||
1. Sign in to Grafana as a server administrator.
|
||||
1. Hover your cursor over the **Server Admin** (shield) icon until a menu appears, and click **Users**.
|
||||
|
||||

|
||||
|
||||
> **Note:** If you have [organization administrator]({{< relref "../../roles-and-permissions/#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../../roles-and-permissions/#grafana-server-administrators" >}}) permissions, you can still [view of list of users in a given organization]({{< relref "../manage-org-users/#view-a-list-of-organization-users" >}}).
|
||||
|
||||
## View user details
|
||||
@@ -62,26 +60,18 @@ A user account contains the following sections.
|
||||
|
||||
This section contains basic user information, which users can update.
|
||||
|
||||

|
||||
|
||||
#### Permissions
|
||||
|
||||
This indicates whether the user account has the Grafana administrator flag applied. If the flag is set to **Yes**, then the user is a Grafana server administrator.
|
||||
|
||||

|
||||
|
||||
#### Organizations
|
||||
|
||||
This section lists the organizations the user belongs to and their assigned role.
|
||||
|
||||

|
||||
|
||||
#### Sessions
|
||||
|
||||
This section includes recent user sessions and information about the time the user logged in and they system they used. You can force logouts, if necessary.
|
||||
|
||||

|
||||
|
||||
## Edit a user account
|
||||
|
||||
Edit a user account when you want to modify user login credentials, or delete, disable, or enable a user.
|
||||
|
||||
@@ -29,9 +29,7 @@ You can change your Grafana password at any time.
|
||||
|
||||
1. Sign in to Grafana.
|
||||
1. Hover your mouse over the user icon in the lower-left corner of the page.
|
||||
1. Click **Change Password**.
|
||||
Grafana opens the **Change Password** tab.
|
||||
|
||||
1. Click **Change Password**. Grafana opens the **Change Password** tab.
|
||||
1. Enter your old password and a new password.
|
||||
1. Confirm your new password.
|
||||
1. Click **Change Password**.
|
||||
@@ -54,6 +52,7 @@ You can choose the way you would like data to appear in Grafana, including the U
|
||||
- **Home dashboard** refers to the dashboard you see when you sign in to Grafana. By default, this is set to the Home dashboard.
|
||||
- **Timezone** is used by dashboards when you set time ranges, so that you view data in your timezone instead of UTC.
|
||||
- **Week start** is the first day of the week you want to use in dashboard time ranges, for example, `This week`.
|
||||
- **Language** determines the language used for parts of the Grafana interface.
|
||||
|
||||
**To edit your preferences**:
|
||||
|
||||
|
||||
@@ -10,18 +10,34 @@ keywords:
|
||||
- guide
|
||||
- rules
|
||||
- create
|
||||
title: Annotations and labels for alerting rules
|
||||
title: Labels and annotations
|
||||
weight: 401
|
||||
---
|
||||
|
||||
# Annotations and labels for alerting rules
|
||||
# Labels and annotations
|
||||
|
||||
Annotations and labels are key value pairs associated with alerts originating from the alerting rule, datasource response, and as a result of alerting rule evaluation. They can be used in alert notifications directly or in templates and template functions to create notification content dynamically.
|
||||
Labels and annotations contain information about an alert. Both labels and annotations have the same structure: a set of named values; however their intended uses are different. An example of label, or the equivalent annotation, might be `alertname="test"`.
|
||||
|
||||
## Annotations
|
||||
The main difference between a label and an annotation is that labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert.
|
||||
|
||||
Annotations are key-value pairs that provide additional meta-information about an alert. You can use the following annotations: `description`, `summary`, `runbook_url`, `alertId`, `dashboardUid`, and `panelId`. For example, a description, a summary, and a runbook URL. These are displayed in rule and alert details in the UI and can be used in contact point message templates.
|
||||
For example, consider two high CPU alerts: one for `server1` and another for `server2`. In such an example we might have a label called `server` where the first alert has the label `server="server1"` and the second alert has the label `server="server2"`. However, we might also want to add a description to each alert such as `"The CPU usage for server1 is above 75%."`, where `server1` and `75%` are replaced with the name and CPU usage of the server (please refer to the documentation on [templating labels and annotations]({{< relref "./variables-label-annotation" >}}) for how to do this). This kind of description would be more suitable as an annotation.
|
||||
|
||||
## Labels
|
||||
|
||||
Labels are key-value pairs that contain information about, and are used to uniquely identify an alert. The label set for an alert is generated and added to throughout the alerting evaluation and notification process.
|
||||
Labels contain information that identifies an alert. An example of a label might be `server=server1`. Each alert can have more than one label, and the complete set of labels for an alert is called its label set. It is this label set that identifies the alert.
|
||||
|
||||
For example, an alert might have the label set `{alertname="High CPU usage",server="server1"}` while another alert might have the label set `{alertname="High CPU usage",server="server2"}`. These are two separate alerts because although their `alertname` labels are the same, their `server` labels are different.
|
||||
|
||||
The label set for an alert is a combination of the labels from the datasource, custom labels from the alert rule, and a number of reserved labels such as `alertname`.
|
||||
|
||||
### Custom Labels
|
||||
|
||||
Custom labels are additional labels from the alert rule. Like annotations, custom labels must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. Documentation on how to template custom labels can be found [here]({{< relref "./variables-label-annotation" >}}).
|
||||
|
||||
When using custom labels with templates it is important to make sure that the label value does not change between consecutive evaluations of the alert rule as this will end up creating large numbers of distinct alerts. However, it is OK for the template to produce different label values for different alerts. For example, do not put the value of the query in a custom label as this will end up creating a new set of alerts each time the value changes. Instead use annotations.
|
||||
|
||||
It is also important to make sure that the label set for an alert does not have two or more labels with the same name. If a custom label has the same name as a label from the datasource then it will replace that label. However, should a custom label have the same name as a reserved label then the custom label will be omitted from the alert.
|
||||
|
||||
## Annotations
|
||||
|
||||
Annotations are named pairs that add additional information to existing alerts. There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like custom labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. If an annotation contains template code, the template is evaluated once when the alert is fired. It is not re-evaluated, even when the alert is resolved. Documentation on how to template annotations can be found [here]({{< relref "./variables-label-annotation" >}}).
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/fundamentals/annotation-label/variables-label-annotation/
|
||||
description: Learn about labels and label matchers in alerting
|
||||
description: Learn about templating of labels and annotations
|
||||
keywords:
|
||||
- grafana
|
||||
- alerting
|
||||
- guide
|
||||
- fundamentals
|
||||
title: How to template annotations and labels
|
||||
title: Templating labels and annotations
|
||||
weight: 117
|
||||
---
|
||||
|
||||
# How to template annotations and labels
|
||||
# Templating labels and annotations
|
||||
|
||||
In Grafana it is possible to template annotations and labels just like you would in Prometheus. Those who have used
|
||||
Prometheus before should be familiar with the `$labels` variable which holds the label key/value pairs of the alert
|
||||
instance and the `$value` variable which holds the evaluated value of the alert instance.
|
||||
In Grafana it is possible to template labels and annotations just like you would in Prometheus. Those who have used Prometheus before should be familiar with the `$labels` variable which holds the label key/value pairs of the alert instance and the `$value` variable which holds the evaluated value of the alert instance.
|
||||
|
||||
In Grafana it is possible to use the same variables from Prometheus to template annotations and labels, even if your
|
||||
alert does not use a Prometheus datasource.
|
||||
In Grafana it is possible to use the same variables from Prometheus to template labels and annotations, even if your alert does not use a Prometheus datasource.
|
||||
|
||||
For example, let's suppose we want to create an alert in Grafana that tells us when one of our instances is down for
|
||||
more than 5 minutes. Like in Prometheus, we can add a summary annotation to show the instance which is down:
|
||||
For example, let's suppose we want to create an alert in Grafana that tells us when one of our instances is down for more than 5 minutes. Like in Prometheus, we can add a summary annotation to show the instance which is down:
|
||||
|
||||
```
|
||||
Instance {{ $labels.instance }} has been down for more than 5 minutes
|
||||
@@ -43,17 +39,13 @@ of the condition at the time the alert fired. For example:
|
||||
|
||||
## Alert rules with two or more queries or expressions
|
||||
|
||||
In the case where an alert rule has two or more queries, or uses reduce and math expressions, it is possible to template
|
||||
the reduced result of each query and expression with the `$values` variable. This variable holds the labels and value of
|
||||
each reduced query, and the results of any math expressions. However, it does not hold the samples for each query.
|
||||
In the case where an alert rule has two or more queries, or uses reduce and math expressions, it is possible to template the reduced result of each query and expression with the `$values` variable. This variable holds the labels and value of each reduced query, and the results of any math expressions. However, it does not hold the samples for each query.
|
||||
|
||||
For example, suppose you have the following alert rule:
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/unified/grafana-alerting-histogram-quantile.png" class="docs-image--no-shadow" caption="An alert rule that uses histogram_quantile to compute 95th percentile" >}}
|
||||
|
||||
Should this rule create an alert instance `$values` will hold the result of the reduce expression `B` and the math
|
||||
expression `C`. It will not hold the results returned by query `A` because query `A` does not return a single value
|
||||
but rather a series of values over time.
|
||||
Should this rule create an alert instance `$values` will hold the result of the reduce expression `B` and the math expression `C`. It will not hold the results returned by query `A` because query `A` does not return a single value but rather a series of values over time.
|
||||
|
||||
If we were to write a summary annotation such as:
|
||||
|
||||
@@ -61,15 +53,13 @@ If we were to write a summary annotation such as:
|
||||
{{ $labels.instance }} has a 95th percentile request latency above 1s: {{ $value }})
|
||||
```
|
||||
|
||||
We would find that because the condition of the alert, the math expression `C` must be a boolean comparison, it must
|
||||
return either a `0` or a `1`. What we want instead is the 95th percentile from the reduce expression `B`:
|
||||
We would find that because the condition of the alert, the math expression `C` must be a boolean comparison, it must return either a `0` or a `1`. What we want instead is the 95th percentile from the reduce expression `B`:
|
||||
|
||||
```
|
||||
{{ $labels.instance }} has a 95th percentile request latency above 1s: {{ $values.B }})
|
||||
```
|
||||
|
||||
We can also show the labels of `B`, however since this alert rule has just one query the labels of `B` are equivalent to
|
||||
`$labels`:
|
||||
We can also show the labels of `B`, however since this alert rule has just one query the labels of `B` are equivalent to `$labels`:
|
||||
|
||||
```
|
||||
{{ $values.B.Labels.instance }} has a 95th percentile request latency above 1s: {{ $values.B }})
|
||||
@@ -78,8 +68,7 @@ We can also show the labels of `B`, however since this alert rule has just one q
|
||||
### No data and execution errors or timeouts
|
||||
|
||||
Should query `A` return no data then the reduce expression `B` will also return no data. This means that
|
||||
`{{ $values.B }}` will be nil. To ensure that annotations and labels can still be templated even when a query returns
|
||||
no data, we can use an if statement to check for `$values.B`:
|
||||
`{{ $values.B }}` will be nil. To ensure that labels and annotations can still be templated even when a query returns no data, we can use an if statement to check for `$values.B`:
|
||||
|
||||
```
|
||||
{{ if $values.B }}{{ $labels.instance }} has a 95th percentile request latency above 1s: {{ $values.B }}){{ end }}
|
||||
@@ -87,15 +76,28 @@ no data, we can use an if statement to check for `$values.B`:
|
||||
|
||||
## Classic conditions
|
||||
|
||||
If the rule uses a classic condition instead of a reduce and math expression, then `$values` contains the combination
|
||||
of the `refID` and position of the condition. For example, `{{ $values.A0 }}` and `{{ $values.A1 }}`.
|
||||
If the rule uses a classic condition instead of a reduce and math expression, then `$values` contains the combination of the `refID` and position of the condition. For example, `{{ $values.A0 }}` and `{{ $values.A1 }}`.
|
||||
|
||||
## Variables
|
||||
|
||||
The following template variables are available when expanding annotations and labels.
|
||||
The following template variables are available when expanding labels and annotations.
|
||||
|
||||
| Name | Description |
|
||||
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. This is unavailable when the rule uses a [classic condition]({{< relref "../../alerting-rules/create-grafana-managed-rule/#single-and-multi-dimensional-rule" >}}). |
|
||||
| $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the reduce or math expression. If the rule uses a classic condition instead of a reduce and math expression, then `$values` contains the combination of the `refID` and position of the condition. |
|
||||
| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. |
|
||||
|
||||
### Labels with dots
|
||||
|
||||
If a label contains a dot (full stop or period) in its name then the following will not work:
|
||||
|
||||
```
|
||||
Instance {{ $labels.instance.name }} has been down for more than 5 minutes
|
||||
```
|
||||
|
||||
This is because we are printing a non-existing field `name` in `$labels.instance` rather than `instance.name` in `$labels`. Instead we can use the `index` function to print `instance.name`:
|
||||
|
||||
```
|
||||
Instance {{ index $labels "instance.name" }} has been down for more than 5 minutes
|
||||
```
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/contact-points/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/contact-points/
|
||||
- /docs/grafana/latest/alerting/fundamentals/contact-points/contact-point-types/
|
||||
description: Create or edit contact point
|
||||
keywords:
|
||||
- grafana
|
||||
- alerting
|
||||
- guide
|
||||
- contact point
|
||||
- notification channel
|
||||
- create
|
||||
title: Contact points
|
||||
weight: 410
|
||||
---
|
||||
|
||||
# Contact points
|
||||
|
||||
Use contact points to define how your contacts are notified when an alert rule fires. A contact point can have one or more contact point types, for example, email, slack, webhook, and so on. When an alert rule fires, a notification is sent to all contact point types listed for a contact point. Contact points can be configured for the Grafana Alertmanager as well as external alertmanagers.
|
||||
|
||||
You can also use message templating to customize notification messages for contact point types.
|
||||
|
||||
## Supported contact point types
|
||||
|
||||
The following table lists the contact point types supported by Grafana.
|
||||
|
||||
| Name | Type | Grafana Alertmanager | Other Alertmanagers |
|
||||
| ------------------------------------------------ | ------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| [DingDing](https://www.dingtalk.com/en) | `dingding` | Supported | N/A |
|
||||
| [Discord](https://discord.com/) | `discord` | Supported | N/A |
|
||||
| [Email](#email) | `email` | Supported | Supported |
|
||||
| [Google Hangouts](https://hangouts.google.com/) | `googlechat` | Supported | N/A |
|
||||
| [Kafka](https://kafka.apache.org/) | `kafka` | Supported | N/A |
|
||||
| [Line](https://line.me/en/) | `line` | Supported | N/A |
|
||||
| [Microsoft Teams](https://teams.microsoft.com/) | `teams` | Supported | N/A |
|
||||
| [Opsgenie](https://atlassian.com/opsgenie/) | `opsgenie` | Supported | Supported |
|
||||
| [Pagerduty](https://www.pagerduty.com/) | `pagerduty` | Supported | Supported |
|
||||
| [Prometheus Alertmanager](https://prometheus.io) | `prometheus-alertmanager` | Supported | N/A |
|
||||
| [Pushover](https://pushover.net/) | `pushover` | Supported | Supported |
|
||||
| [Sensu Go](https://docs.sensu.io/sensu-go/) | `sensugo` | Supported | N/A |
|
||||
| [Slack](https://slack.com/) | `slack` | Supported | Supported |
|
||||
| [Telegram](https://telegram.org/) | `telegram` | Supported | N/A |
|
||||
| [Threema](https://threema.ch/) | `threema` | Supported | N/A |
|
||||
| [VictorOps](https://help.victorops.com/) | `victorops` | Supported | Supported |
|
||||
| [Webhook](#webhook) | `webhook` | Supported | Supported ([different format](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config)) |
|
||||
| [Cisco Webex Teams](#webex) | `webex` | Supported | Supported |
|
||||
| [WeCom](#wecom) | `wecom` | Supported | N/A |
|
||||
| [Zenduty](https://www.zenduty.com/) | `webhook` | Supported | N/A |
|
||||
|
||||
## Useful links
|
||||
|
||||
[Manage contact points](https://grafana.com/docs/grafana/next/alerting/manage-notifications/create-contact-point/)
|
||||
|
||||
[Create and edit message templates](https://grafana.com/docs/grafana/next/alerting/manage-notifications/create-message-template/)
|
||||
91
docs/sources/alerting/fundamentals/notifications/index.md
Normal file
91
docs/sources/alerting/fundamentals/notifications/index.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/notifications/
|
||||
- /docs/grafana/latest/alerting/contact-points/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/contact-points/
|
||||
- /docs/grafana/latest/alerting/fundamentals/contact-points/contact-point-types/
|
||||
description: Create or edit contact point
|
||||
keywords:
|
||||
- grafana
|
||||
- alerting
|
||||
- guide
|
||||
- contact point
|
||||
- notification channel
|
||||
- create
|
||||
title: Notifications
|
||||
weight: 410
|
||||
---
|
||||
|
||||
# Notifications
|
||||
|
||||
Notifications are sent when an alert is firing or has been resolved. You use notification policies to configure how and where a notification is sent; how often a notification should be sent; and whether alerts should all be sent in the same notification, sent in grouped notifications based on a set of labels, or as separate notifications.
|
||||
|
||||
## Notification policies
|
||||
|
||||
Notification policies control when and where notifications are sent. A notification policy can choose to send all alerts together in the same notification, send alerts in grouped notifications based on a set of labels, or send alerts as separate notifications. You can configure each notification policy to control how often notifications should be sent as well as having one or more mute timings to inhibit notifications at certain times of the day and on certain days of the week.
|
||||
|
||||
Notification policies are organized in a tree structure where at the root of the tree there is a notification policy called the root policy. There can be only one root policy and the root policy cannot be deleted.
|
||||
|
||||
Specific routing policies are descendents of the root policy and can be used to match either all alerts or a subset of alerts based on a set of matching labels. A notification policy matches an alert when its matching labels match the labels in the alert.
|
||||
|
||||
A specific routing policy can have its own descendent policies, called nested policies, which allow for additional matching of alerts. An example of a specific routing policy could be sending infrastructure alerts to the Ops team; while a descendent policy might send high priority alerts to Pagerduty and low priority alerts as emails.
|
||||
|
||||
All alerts, irrespective of their labels, match the root policy. However, when the root policy receives an alert it looks at each specific routing policy and sends the alert to the first specific routing policy that matches the alert. If the specific routing policy has further descendent policies, then it can attempt to the match the alert against one of its nested policies. If no nested policies match the alert then the specific routing policy is the matching policy. If there are no specific routing policies, or no specific routing policies match the alert, then the root policy is the matching policy.
|
||||
|
||||
More information on how to configure notification policies can be found [here]({{< relref "../../manage-notifications/create-notification-policy/" >}}).
|
||||
|
||||
## Contact points
|
||||
|
||||
Contact points contain the configuration for sending notifications. A contact point is a list of integrations, each of which sends a notification to a particular email address, service or URL. Contact points can have multiple integrations of the same kind, or a combination of integrations of different kinds. For example, a contact point could contain a Pagerduty integration; an email and Slack integration; or a Pagerduty integration, a Slack integration, and two email integrations. You can also configure a contact point with no integrations; in which case no notifications are sent.
|
||||
|
||||
A contact point cannot send notifications until it has been added to a notification policy. A notification policy can only send alerts to one contact point, but a contact point can be added to a number of notification policies at the same time. When an alert matches a notification policy, the alert is sent to the contact point in that notification policy, which then sends a notification to each integration in its configuration.
|
||||
|
||||
### Supported integrations
|
||||
|
||||
The following table contains the integrations supported in Grafana:
|
||||
|
||||
| Name | Type | Grafana Alertmanager | Other Alertmanagers |
|
||||
| ------------------------------------------------ | ------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| [DingDing](https://www.dingtalk.com/en) | `dingding` | Supported | N/A |
|
||||
| [Discord](https://discord.com/) | `discord` | Supported | N/A |
|
||||
| [Email](#email) | `email` | Supported | Supported |
|
||||
| [Google Hangouts](https://hangouts.google.com/) | `googlechat` | Supported | N/A |
|
||||
| [Kafka](https://kafka.apache.org/) | `kafka` | Supported | N/A |
|
||||
| [Line](https://line.me/en/) | `line` | Supported | N/A |
|
||||
| [Microsoft Teams](https://teams.microsoft.com/) | `teams` | Supported | N/A |
|
||||
| [Opsgenie](https://atlassian.com/opsgenie/) | `opsgenie` | Supported | Supported |
|
||||
| [Pagerduty](https://www.pagerduty.com/) | `pagerduty` | Supported | Supported |
|
||||
| [Prometheus Alertmanager](https://prometheus.io) | `prometheus-alertmanager` | Supported | N/A |
|
||||
| [Pushover](https://pushover.net/) | `pushover` | Supported | Supported |
|
||||
| [Sensu Go](https://docs.sensu.io/sensu-go/) | `sensugo` | Supported | N/A |
|
||||
| [Slack](https://slack.com/) | `slack` | Supported | Supported |
|
||||
| [Telegram](https://telegram.org/) | `telegram` | Supported | N/A |
|
||||
| [Threema](https://threema.ch/) | `threema` | Supported | N/A |
|
||||
| [VictorOps](https://help.victorops.com/) | `victorops` | Supported | Supported |
|
||||
| [Webhook](#webhook) | `webhook` | Supported | Supported ([different format](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config)) |
|
||||
| [Cisco Webex Teams](#webex) | `webex` | Supported | Supported |
|
||||
| [WeCom](#wecom) | `wecom` | Supported | N/A |
|
||||
| [Zenduty](https://www.zenduty.com/) | `webhook` | Supported | N/A |
|
||||
|
||||
## Templating notifications
|
||||
|
||||
You can customize notifications with templates. For example, templates can be used to change the subject and message of an email, or the title and message of notifications sent to Slack.
|
||||
|
||||
Templates are not limited to an individual integration or contact point, but instead can be used in a number of integrations in the same contact point and even integrations across different contact points. For example, a Grafana user can create a template called `custom_subject_or_title` and use it for both templating subjects in emails and titles of Slack messages without having to create two separate templates.
|
||||
|
||||
All notifications templates are written in [Go's templating language](https://pkg.go.dev/text/template), and are in the Contact points tab on the Alerting page.
|
||||
|
||||
More information on how to template notifications can be found [here]({{< relref "../../manage-notifications/create-message-template/" >}}).
|
||||
|
||||
## Silences
|
||||
|
||||
You can use silences to mute notifications from one or more firing rules. Silences do not stop alerts from firing or being resolved, or hide firing alerts in the user interface. A silence lasts as long as its duration which can be configured in minutes, hours, days, months or years.
|
||||
|
||||
More information on how to configure silences can be found [here]({{< relref "../../manage-notifications/create-silence/" >}}).
|
||||
|
||||
## Useful links
|
||||
|
||||
- [Notification policies]({{< relref "../../manage-notifications/create-notification-policy/" >}})
|
||||
- [Contact points]({{< relref "../../manage-notifications/create-contact-point/" >}})
|
||||
- [Templating notifications]({{< relref "../../manage-notifications/create-message-template/" >}})
|
||||
- [Silences]({{< relref "../../manage-notifications/create-silence/" >}})
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/silences/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/silences/
|
||||
- /docs/grafana/latest/alerting/fundamentals/silences
|
||||
description: Silences
|
||||
keywords:
|
||||
- grafana
|
||||
- alerting
|
||||
- silence
|
||||
- mute
|
||||
title: Silences
|
||||
weight: 420
|
||||
---
|
||||
|
||||
# Silences
|
||||
|
||||
Use silences to stop notifications from one or more alerting rules. Silences do not prevent alert rules from being evaluated. They also do not stop alert instances from being shown in the user interface. Silences only stop notifications from getting created. A silence lasts for only a specified window of time.
|
||||
|
||||
You can configure Grafana managed silences as well as silences for an external Alertmanager data source.
|
||||
|
||||
## Useful links
|
||||
|
||||
[Create silences](https://grafana.com/docs/grafana/latest/alerting/manage-notifications/create-silence/)
|
||||
@@ -12,34 +12,58 @@ weight: 460
|
||||
|
||||
# Use images in notifications
|
||||
|
||||
Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including an image of the panel associated with the Grafana managed alert rule.
|
||||
Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including a screenshot of the panel associated with the alert.
|
||||
|
||||
> **Note**: Images in notifications are not available for Grafana Mimir and Loki managed alert rules, or when Grafana is set up to send alert notifications to an external Alertmanager.
|
||||
> **Note**: This feature is not supported for Mimir or Loki rules, or when Grafana sends alert notifications to an external Alertmanager.
|
||||
|
||||
If Grafana is set up to send images in notifications, it takes a screenshot of the panel for the Grafana managed alert rule when either of the following happen:
|
||||
When an alert is fired or resolved Grafana takes a screenshot of the panel associated with the alert. This is determined via the Dashboard UID and Panel ID annotations of the rule. Grafana cannot take a screenshot for alerts that are not associated with a panel.
|
||||
|
||||
1. The alert rule transitions from pending to firing
|
||||
2. The alert rule transitions from firing to OK
|
||||
Because a number of contact points, such as email, do not support uploading screenshots at the time of sending a notification; Grafana can also upload the screenshot to a cloud storage service such as Amazon S3, Azure Blob Storage and Google Cloud Storage, where a link to the uploaded screenshot can be added to the notification. However, if using a cloud storage service is not an option then Grafana can be its own cloud storage service such that the screenshot is available under the same domain as Grafana.
|
||||
|
||||
Grafana does not support images for alert rules that are not associated with a panel. An alert rule is associated with a panel when it has both Dashboard UID and Panel ID annotations.
|
||||
Should either the cloud storage service, or Grafana if acting as its own cloud storage service, be protected by a firewall, gateway service or VPN, then screenshots might not be shown in notifications.
|
||||
|
||||
Images are stored in the [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) path and so Grafana must have write-access to this path. If Grafana cannot write to this path then screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt. In addition to storing images on disk, Grafana can also store the image in an external image store such as Amazon S3, Azure Blob Storage, Google Cloud Storage and even Grafana where screenshots are stored in `public/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from disk but not the external image store. If Grafana is the external image store then screenshots are deleted from `data` but not from `public/img/attachments`.
|
||||
How to choose between uploading screenshots at the time of sending the notification, using a cloud storage service, or using Grafana as its own cloud storage service, depends on which contact points you plan to use and whether you use a firewall, gateway service or VPN.
|
||||
|
||||
> **Note**: It is recommended that you use an external image store, as not all contact points support uploading images from disk. It is also possible that the image on disk is deleted before an alert notification is sent if `temp_data_lifetime` is less than the `group_wait` and `group_interval` options used in Alertmanager.
|
||||
For example, if a contact point supports uploading images at the time of notification is it not required to use cloud storage. Cloud storage is required when a contact point does not support uploading images at the time of sending a notification, such as email. We don't recommend using cloud storage if the cloud storage service is behind a firewall, gateway service, or VPN, as screenshots might not be shown in notifications.
|
||||
|
||||
Please refer to the table at the end of this page for a list of contact points and their support for images in notifications.
|
||||
|
||||
## Requirements
|
||||
|
||||
To use images in notifications, Grafana must be set up to use [image rendering](https://grafana.com/docs/grafana/next/setup-grafana/image-rendering/). It is also recommended that Grafana is set up to upload images to an external image store, such as Amazon S3, Azure Blob Storage, Google Cloud Storage or even Grafana.
|
||||
To use images in notifications, Grafana must be set up to use [image rendering](https://grafana.com/docs/grafana/next/setup-grafana/image-rendering/). You can either install the image rendering plugin or run it as a remote rendering service.
|
||||
|
||||
When a screenshot is taken it is saved to the [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) path. This is where screenshots are stored before being sent in a notification or uploaded to a cloud storage service. Grafana must have write-access to this path. If Grafana cannot write to this path then screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt.
|
||||
|
||||
If using a [cloud storage service](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storage) such as Amazon S3, Azure Blob Storage or Google Cloud Storage, uploaded images need to be accessible outside of a firewall, gateway service or VPN for screenshots to be shown in notifications. Grafana will not delete screenshots from cloud storage. We recommend configuring a retention policy on the bucket to delete screenshots older than 1 month.
|
||||
|
||||
If using Grafana as its own cloud storage service then screenshots will be saved to `static_root_path/img/attachments`. `static_root_path` is a configuration option for Grafana and can be found in `defaults.ini`. However, like when using a cloud storage service, images need to be accessible outside of a firewall, gateway service or VPN for screenshots to be shown in notifications.
|
||||
|
||||
When using Grafana as its own cloud storage service screenshots are copied from [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) to `static_root_path/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from [data]({{< relref "../../setup-grafana/configure-grafana/#paths" >}}) but not from `static_root_path/images/attachments`. To delete screenshots from `static_root_path` after a certain amount of time we recommend setting up a CRON job.
|
||||
|
||||
## Configuration
|
||||
|
||||
If Grafana has been set up to use image rendering, images in notifications can be turned on via the `capture` option in `[unified_alerting.screenshots]`:
|
||||
Having installed either the image rendering plugin, or set up Grafana to use a remote rendering service, set `capture` in `[unified_alerting.screenshots]` to `true`:
|
||||
|
||||
# Enable screenshots in notifications. This option requires the Grafana Image Renderer plugin.
|
||||
# For more information on configuration options, refer to [rendering].
|
||||
capture = true
|
||||
capture = false
|
||||
|
||||
It is recommended that `max_concurrent_screenshots` is set to a value that is less than or equal to `concurrent_render_request_limit`. The default value for both `max_concurrent_screenshots` and `concurrent_render_request_limit` is `5`:
|
||||
If screenshots should be uploaded to cloud storage then `upload_external_image_storage` should also be set to `true`:
|
||||
|
||||
# Uploads screenshots to the local Grafana server or remote storage such as Azure, S3 and GCS. Please
|
||||
# see [external_image_storage] for further configuration options. If this option is false, screenshots
|
||||
# will be persisted to disk for up to temp_data_lifetime.
|
||||
upload_external_image_storage = false
|
||||
|
||||
Please see [`[external_image_storage]`](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storage) for instructions on how to configure cloud storage. Grafana will not start if `upload_external_image_storage` is `true` and `[external_image_storage]` contains missing or invalid configuration.
|
||||
|
||||
If Grafana is acting as its own cloud storage then `[upload_external_image_storage]` should be set to `true` and the `local` provider should be set in [`[external_image_storage]`](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#external_image_storage).
|
||||
|
||||
Restart Grafana for the changes to take effect.
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
We recommended that `max_concurrent_screenshots` is less than or equal to `concurrent_render_request_limit`. The default value for both `max_concurrent_screenshots` and `concurrent_render_request_limit` is `5`:
|
||||
|
||||
# The maximum number of screenshots that can be taken at the same time. This option is different from
|
||||
# concurrent_render_request_limit as max_concurrent_screenshots sets the number of concurrent screenshots
|
||||
@@ -47,57 +71,60 @@ It is recommended that `max_concurrent_screenshots` is set to a value that is le
|
||||
# the total number of concurrent screenshots across all Grafana services.
|
||||
max_concurrent_screenshots = 5
|
||||
|
||||
If Grafana has been set up to use an external image store, `upload_external_image_storage` should be set to `true`:
|
||||
## Support for images in contact points
|
||||
|
||||
# Uploads screenshots to the local Grafana server or remote storage such as Azure, S3 and GCS. Please
|
||||
# see [external_image_storage] for further configuration options. If this option is false, screenshots
|
||||
# will be persisted to disk for up to temp_data_lifetime.
|
||||
upload_external_image_storage = false
|
||||
Grafana supports a wide range of contact points with varied support for images in notifications. The table below shows the list of all contact points supported in Grafana and their support for uploading images at the time of sending the notification and images uploaded to cloud storage, including when Grafana is acting as its own cloud storage service.
|
||||
|
||||
Restart Grafana for the changes to take affect.
|
||||
| Name | Upload image at time of notification | Cloud storage |
|
||||
| ----------------------- | ------------------------------------ | ------------- |
|
||||
| DingDing | No | No |
|
||||
| Discord | Yes | Yes |
|
||||
| Email | Yes | Yes |
|
||||
| Google Hangouts Chat | No | Yes |
|
||||
| Kafka | No | No |
|
||||
| Line | No | No |
|
||||
| Microsoft Teams | No | Yes |
|
||||
| Opsgenie | No | Yes |
|
||||
| Pagerduty | No | Yes |
|
||||
| Prometheus Alertmanager | No | No |
|
||||
| Pushover | Yes | No |
|
||||
| Sensu Go | No | No |
|
||||
| Slack | No (will be available in 9.4) | Yes |
|
||||
| Telegram | Yes | No |
|
||||
| Threema | No | No |
|
||||
| VictorOps | No | No |
|
||||
| Webhook | No | Yes |
|
||||
| Cisco Webex Teams | No | Yes |
|
||||
|
||||
## Supported notifiers
|
||||
## Limitations
|
||||
|
||||
Images in notifications are supported in the following notifiers and additional support will be added in the future:
|
||||
- This feature is not supported for Mimir or Loki rules, or when Grafana sends alert notifications to an external Alertmanager.
|
||||
- When multiple alerts are sent in a single notification a screenshot might be included for each alert. The order the images are shown in random.
|
||||
- Some contact points support at most one image per notification. In this case, the first image associated with an alert will be attached.
|
||||
- We don't recommend using cloud storage if the cloud storage service is behind a firewall, gateway service, or VPN, as screenshots might not be shown in notifications.
|
||||
|
||||
| Name | Upload images from disk | Include images from URL |
|
||||
| ----------------------- | ----------------------- | ----------------------- |
|
||||
| DingDing | No | No |
|
||||
| Discord | Yes | Yes |
|
||||
| Email | Yes | Yes |
|
||||
| Google Hangouts Chat | No | Yes |
|
||||
| Kafka | No | No |
|
||||
| Line | No | No |
|
||||
| Microsoft Teams | No | Yes |
|
||||
| Opsgenie | No | Yes |
|
||||
| Pagerduty | No | Yes |
|
||||
| Prometheus Alertmanager | No | No |
|
||||
| Pushover | Yes | No |
|
||||
| Sensu Go | No | No |
|
||||
| Slack | No | Yes |
|
||||
| Telegram | No | No |
|
||||
| Threema | No | No |
|
||||
| VictorOps | No | No |
|
||||
| Webhook | No | Yes |
|
||||
| Cisco Webex Teams | No | Yes |
|
||||
## Troubleshooting
|
||||
|
||||
Include images from URL refers to using the external image store.
|
||||
If Grafana has been set up to send images in notifications, however notifications are still being received without them, follow the troubleshooting steps below:
|
||||
|
||||
1. Check that images in notifications has been set up as per the instructions.
|
||||
2. Enable debug logging in Grafana and look for logs with the logger `ngalert.image`.
|
||||
3. If the alert is not associated with a dashboard there will be logs for `Cannot take screenshot for alert rule as it is not associated with a dashboard`.
|
||||
4. If the alert is associated with a dashboard, but no panel in the dashboard, there will be logs for `Cannot take screenshot for alert rule as it is not associated with a panel`.
|
||||
5. If images cannot be taken because of mis-configuration or an issue with image rendering there will be logs for `Failed to take an image` including the Dashboard UID, Panel ID, and the error message.
|
||||
6. Check that the contact point supports images in notifications, and the present configuration, as per the table.
|
||||
7. If the image was uploaded to cloud storage make sure it is public.
|
||||
8. If images are made available via Grafana's built in web server make sure it is accessible via the Internet.
|
||||
|
||||
## Metrics
|
||||
|
||||
Grafana provides the following metrics to observe the performance and failure rate of images in notifications.
|
||||
For example, if a screenshot could not be taken within the expected time (10 seconds) then the counter `grafana_screenshot_failures_total` is updated.
|
||||
|
||||
- `grafana_screenshot_cache_hits_total`
|
||||
- `grafana_screenshot_cache_misses_total`
|
||||
- `grafana_alerting_image_cache_hits_total`
|
||||
- `grafana_alerting_image_cache_misses_total`
|
||||
- `grafana_screenshot_duration_seconds`
|
||||
- `grafana_screenshot_failures_total`
|
||||
- `grafana_screenshot_successes_total`
|
||||
- `grafana_screenshot_upload_failures_total`
|
||||
- `grafana_screenshot_upload_successes_total`
|
||||
|
||||
## Limitations
|
||||
|
||||
- Images in notifications are not available for Grafana Mimir and Loki managed alert rules, or when Grafana is set up to send alert notifications to an external Alertmanager.
|
||||
- When alerts generated by different alert rules are sent in a single notification, there may be screenshots for each alert rule. This happens if an alert group contains multiple alerting rules. The order the images are attached is random. If you need to guarantee the ordering of images, make sure that your alert groups contain a single alerting rule.
|
||||
- Some contact points only handle a single image. In this case, the first image associated with an alert will be attached. Because the ordering is random, this may not always be an image for the same alert rule. If you need to guarantee you receive a screenshot for a particular rule, make sure that your alert groups contain a single alerting rule.
|
||||
|
||||
@@ -168,6 +168,21 @@ You can attach these permissions to the IAM role or IAM user you configured in [
|
||||
}
|
||||
```
|
||||
|
||||
**Cross-account observability:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": ["oam:ListSinks", "oam:ListAttachedLinks"],
|
||||
"Effect": "Allow",
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Configure CloudWatch settings
|
||||
|
||||
#### Namespaces of Custom Metrics
|
||||
|
||||
@@ -214,6 +214,33 @@ When making `stats` queries in [Explore]({{< relref "../../../explore/" >}}), ma
|
||||
|
||||
{{< figure src="/static/img/docs/v70/explore-mode-switcher.png" max-width="500px" class="docs-image--right" caption="Explore mode switcher" >}}
|
||||
|
||||
## Cross-account observability
|
||||
|
||||
The CloudWatch plugin provides the ability to monitor and troubleshoot applications that span across multiple accounts within a region. Using cross-account observability, you can seamlessly search, visualize and analyze metrics and logs, without having to worry about account boundaries.
|
||||
|
||||
> **Note:** This feature is currently behind the `cloudWatchCrossAccountQuerying` feature toggle.
|
||||
|
||||
> You can enable feature toggles through configuration file or environment variables. See configuration [docs]({{< relref "../setup-grafana/configure-grafana/#feature_toggles" >}}) for details.
|
||||
> Grafana Cloud users can access this feature by [opening a support ticket in the Cloud Portal](https://grafana.com/profile/org#support).
|
||||
|
||||
### Getting started
|
||||
|
||||
To enable cross-account observability, first enable it in CloudWatch using the official [CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html), then add [two new API actions]({{< relref "../#cross-account-observability" >}}) to the IAM policy attached to the role/user running the plugin.
|
||||
|
||||
Cross-account querying is available in the plugin through the `Logs` mode and the `Metric search` mode. Once you have it configured correctly, you'll see a "Monitoring account" badge displayed in the query editor header.
|
||||
|
||||
{{< figure src="/static/img/docs/cloudwatch/cloudwatch-monitoring-badge-9.3.0.png" max-width="1200px" caption="Monitoring account badge" >}}
|
||||
|
||||
### Metrics editor
|
||||
|
||||
When you select the `Builder` mode within the Metric search editor, a new Account field displays. Use the Account field to specify which of the linked accounts to target for the given query. By default, the `All` option is specified, which will target all linked accounts.
|
||||
|
||||
While in `Code` mode, you can specify any math expression. If the Monitoring account badge displays in the query editor header, all `SEARCH` expressions entered in this field will be cross-account by default. You can limit the search to one or a set of accounts, as documented in the [AWS documentation](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html).
|
||||
|
||||
### Logs editor
|
||||
|
||||
The Log group selector allows you to specify what log groups to target in the logs query. If the Monitoring account badge is displayed in the query editor header, it is possible to search and select log groups across multiple accounts. You can use the Account field in the Log Group Selector to filter Log Groups by Account. If you have many log groups and do not see the log group you'd like to select in the selector, use the prefix search to narrow down the possible log groups.
|
||||
|
||||
### Deep-link Grafana panels to the CloudWatch console
|
||||
|
||||
{{< figure src="/static/img/docs/v70/cloudwatch-logs-deep-linking.png" max-width="500px" class="docs-image--right" caption="CloudWatch Logs deep linking" >}}
|
||||
|
||||
@@ -40,11 +40,9 @@ Public plugins need to be reviewed by the Grafana team before you can sign them.
|
||||
|
||||
```bash
|
||||
export GRAFANA_API_KEY=<YOUR_API_KEY>
|
||||
npx @grafana/sign-plugin plugin:sign
|
||||
npx @grafana/sign-plugin
|
||||
```
|
||||
|
||||
> **Note:** If running NPM 7+ the `npx` commands mentioned in this article may hang. The workaround is to use `npx --legacy-peer-deps <command to run>`.
|
||||
|
||||
## Sign a private plugin
|
||||
|
||||
1. In your plugin directory, sign the plugin with the API key you just created. Grafana Sign Plugin creates a [MANIFEST.txt](#plugin-manifest) file in the `dist` directory of your plugin.
|
||||
@@ -53,7 +51,7 @@ Public plugins need to be reviewed by the Grafana team before you can sign them.
|
||||
|
||||
```bash
|
||||
export GRAFANA_API_KEY=<YOUR_API_KEY>
|
||||
npx @grafana/sign-plugin plugin:sign --rootUrls https://example.com/grafana
|
||||
npx @grafana/sign-plugin --rootUrls https://example.com/grafana
|
||||
```
|
||||
|
||||
## Plugin signature levels
|
||||
|
||||
@@ -61,7 +61,7 @@ Grafana Enterprise adds the following features:
|
||||
- [Custom branding]({{< relref "../setup-grafana/configure-grafana/configure-custom-branding/" >}}) to customize Grafana from the brand and logo to the footer links.
|
||||
- [Usage insights]({{< relref "../dashboards/assess-dashboard-usage/" >}}) to understand how your Grafana instance is used.
|
||||
- [Recorded queries]({{< relref "../administration/recorded-queries" >}}) to see trends over time for your data sources.
|
||||
- [Vault integration]({{< relref "../setup-grafana/configure-security/configure-database-encryption/encrypt-secrets-using-hashicorp-key-vault/" >}}) to manage your configuration or provisioning secrets with Vault.
|
||||
- [Vault integration]({{< relref "../setup-grafana/configure-security/configure-database-encryption/#encrypting-your-database-with-a-key-from-a-key-management-service-kms" >}}) to manage your configuration or provisioning secrets with Vault.
|
||||
- [Auditing]({{< relref "../setup-grafana/configure-security/audit-grafana/" >}}) tracks important changes to your Grafana instance to help you manage and mitigate suspicious activity and meet compliance requirements.
|
||||
- [Request security]({{< relref "../setup-grafana/configure-security/configure-request-security/" >}}) makes it possible to restrict outgoing requests from the Grafana server.
|
||||
- [Settings updates at runtime]({{< relref "../setup-grafana/configure-grafana/settings-updates-at-runtime" >}}) allows you to update Grafana settings at runtime without requiring a restart.
|
||||
|
||||
@@ -747,6 +747,10 @@ Text used as placeholder text on login page for password input.
|
||||
|
||||
Set the default UI theme: `dark` or `light`. Default is `dark`.
|
||||
|
||||
### default_language
|
||||
|
||||
This setting configures the default UI language, which must be a supported IETF language tag, such as `en-US`.
|
||||
|
||||
### home_page
|
||||
|
||||
Path to a custom home page. Users are only redirected to this if the default home dashboard is used. It should match a frontend route and contain a leading slash.
|
||||
|
||||
@@ -68,6 +68,7 @@ For a complete list of every change, with links to pull requests and related iss
|
||||
|
||||
## Grafana 9
|
||||
|
||||
- [What's new in 9.3]({{< relref "whats-new-in-v9-3/" >}})
|
||||
- [What's new in 9.2]({{< relref "whats-new-in-v9-2/" >}})
|
||||
- [What's new in 9.1]({{< relref "whats-new-in-v9-1/" >}})
|
||||
- [What's new in 9.0]({{< relref "whats-new-in-v9-0/" >}})
|
||||
|
||||
258
docs/sources/whatsnew/whats-new-in-v9-3.md
Normal file
258
docs/sources/whatsnew/whats-new-in-v9-3.md
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
_build:
|
||||
list: false
|
||||
aliases:
|
||||
- /docs/grafana/latest/guides/whats-new-in-v9-3/
|
||||
description: Feature and improvement highlights for Grafana v9.3
|
||||
keywords:
|
||||
- grafana
|
||||
- new
|
||||
- documentation
|
||||
- '9.3'
|
||||
- release notes
|
||||
title: What's new in Grafana v9.3
|
||||
weight: -33
|
||||
---
|
||||
|
||||
# What’s new in Grafana v9.3
|
||||
|
||||
Welcome to Grafana 9.3! Read on to learn about our navigation overhaul, support for four new languages, new panels and transformations, several often-requested auth improvements, usability improvements to Alerting, and more. For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/master/CHANGELOG.md).
|
||||
|
||||
## New navigation
|
||||
|
||||
Available in **beta** in all editions of Grafana
|
||||
|
||||
Use Grafana’s redesigned navigation to get full visibility into the health of your systems, by quickly jumping between features as part of your incident response workflow.
|
||||
|
||||
As Grafana has grown from a data visualization tool to an observability solution, we’ve added many new features along the way. This has resulted in pages that are visually inconsistent or hard to find. These updates to navigation give Grafana a new look and feel and make page layouts and navigation patterns more consistent.
|
||||
|
||||
We’ve revamped the navigation menu and grouped related tools together, making it easier to find what you need. Pages in Grafana now leverage new layouts that include breadcrumbs and a sidebar, allowing you to quickly jump between pages. We’ve also introduced a header that appears on all pages in Grafana, making dashboard search accessible from any page.
|
||||
|
||||
To try out Grafana’s new navigation, enable the `topnav` feature toggle. If you are a Cloud Advanced customer, open a ticket with our support team and we will enable it for you.
|
||||
|
||||
**Note:** The Grafana and Grafana Cloud documentation has not yet been updated to reflect changes to the navigation - these changes will roll out when the new navigation becomes generally available.
|
||||
|
||||
{{< figure src="/static/img/docs/navigation/navigation-9-3.png" max-width="750px" caption="New navigation for Grafana" >}}
|
||||
|
||||
## View dashboards in Spanish, French, German, and Simplified Chinese
|
||||
|
||||
Generally available in all editions of Grafana
|
||||
|
||||
We have added four new languages to Grafana: Spanish, French, German, and Simplified Chinese.
|
||||
|
||||
With millions of users across the globe, Grafana has a global footprint. In order to make it accessible to a wider audience, we have taken the first steps in localizing key workflows. You can now set Grafana’s language for the navigation, viewing dashboards, and some settings. This will cover the main activities a Viewer performs within Grafana.
|
||||
|
||||
Read more about configuring the [default language for your organization]({{< relref "../administration/organization-preferences/" >}}) and [updating your profile]({{< relref "../administration/user-management/user-preferences/" >}}) in our documentation.
|
||||
|
||||
{{< figure src="/static/img/docs/internationalization/internationalization-9-3.png" max-width="750px" caption="Grafana available in Spanish, French, German, and Simplified Chinese" >}}
|
||||
|
||||
## Geomap panel
|
||||
|
||||
Generally available in all editions of Grafana
|
||||
|
||||
We have added a new alpha layer type in Geomap called photo layer. This layer enables you to render a photo at each data point. To learn more about the photo layer and the geomap panel, refer to [Photos layer]({{< relref "../panels-visualizations/visualizations/geomap/#photos-layer-alpha" >}}).
|
||||
|
||||
{{< figure src="/static/img/docs/geomap-panel/geomap-photos-9-3-0.png" max-width="750px" caption="Geomap panel photos layer" >}}
|
||||
|
||||
## Canvas panel
|
||||
|
||||
Available in **beta** in all editions of Grafana
|
||||
|
||||
Canvas is a new panel that combines the power of Grafana with the flexibility of custom elements. Canvas visualizations are extensible form-built panels that allow you to explicitly place elements within static and dynamic layouts. This empowers you to design custom visualizations and overlay data in ways that aren’t possible with standard Grafana panels, all within Grafana’s UI. If you’ve used popular UI and web design tools, then designing Canvas panels will feel very familiar.
|
||||
|
||||
In Grafana v9.3, we have added icon value mapping support to the Canvas panel. This enables you to dynamically set which icon to display based on your data. To learn more about the Canvas panel, refer to [Canvas]({{< relref "../panels-visualizations/visualizations/canvas" >}}).
|
||||
|
||||
{{< video-embed src="/static/img/docs/canvas-panel/canvas-icon-value-mapping-support-9-3-0.mp4" max-width="750px" caption="Canvas panel icon value mapping support" >}}
|
||||
|
||||
## Public dashboards improvements
|
||||
|
||||
We've made the following improvements to public dashboards.
|
||||
|
||||
### Manage all of your public dashboards in one place
|
||||
|
||||
Available in **experimental** in Grafana Open Source, Enterprise, and Cloud Advanced
|
||||
|
||||
You can use Public Dashboards to make a given dashboard available to anyone on the internet without needing to sign in. In Grafana v9.3, we have introduced a new screen where you can manage all of your public dashboards. From here, you can view a list of all of the public dashboards in your Grafana instance, navigate to the underlying dashboard, see if it is enabled, link out to the public version of the dashboard, or update the public dashboard's configuration. You can see a public dashboard's configuration if you have view access to the dashboard itself, and you can edit its configuration if you have the Admin or Server Admin role or the "Public Dashboard writer" role if you are using RBAC in Grafana Enterprise or Cloud Advanced.
|
||||
|
||||
To check out this new screen and configure your public dashboards, navigate to **Dashboards > Public Dashboards**.
|
||||
|
||||
### Choose to display annotations in public dashboards
|
||||
|
||||
Available in **experimental** in Grafana Open Source, Enterprise, and Cloud Advanced
|
||||
|
||||
Annotations are now supported in public dashboards, with the exception of query annotations. They are turned off by default, but can be turned on in your public dashboard settings.
|
||||
|
||||
Note that because Public Dashboards is an experimental feature, you need to enable it in Grafana using the `publicDashboards` [feature toggle]({{< relref "../setup-grafana/configure-grafana/#feature_toggles" >}}), or open a support ticket requesting public dashboards if you are a Cloud Advanced customer.
|
||||
|
||||
To learn more about public dashboards, refer to [Public dashboards]({{< relref "../dashboards/dashboard-public/" >}}).
|
||||
|
||||
## New transformation: Partition by values
|
||||
|
||||
Available in **experimental** in all editions of Grafana
|
||||
|
||||
This new transformation can help eliminate the need for multiple queries to the same datasource with different WHERE clauses when graphing multiple series.
|
||||
|
||||
Consider a metrics SQL table with the following data:
|
||||
|
||||
| Time | Region | Value |
|
||||
| ------------------- | ------ | ----- |
|
||||
| 2022-10-20 12:00:00 | US | 1520 |
|
||||
| 2022-10-20 12:00:00 | EU | 2936 |
|
||||
| 2022-10-20 01:00:00 | US | 1327 |
|
||||
| 2022-10-20 01:00:00 | EU | 912 |
|
||||
|
||||
Prior to v9.3, if you wanted to plot a red trendline for US and a blue one for EU in the same TimeSeries panel, you would likely have to split this into two queries:
|
||||
|
||||
```
|
||||
SELECT Time, Value FROM metrics WHERE Time > ‘2022-10-20’ AND Region=’US’
|
||||
SELECT Time, Value FROM metrics WHERE Time > ‘2022-10-20’ AND Region=’EU’
|
||||
```
|
||||
|
||||
This approach also requires you to know ahead of time which regions exist in the metrics table.
|
||||
|
||||
With the partition by values transformer, you can issue a single query and split the results by unique (enum) values from one or more columns (fields) of your choosing. In this case, Region.
|
||||
|
||||
```
|
||||
SELECT Time, Region, Value FROM metrics WHERE Time > ‘2022-10-20’
|
||||
```
|
||||
|
||||
| Time | Region | Value |
|
||||
| ------------------- | ------ | ----- |
|
||||
| 2022-10-20 12:00:00 | US | 1520 |
|
||||
| 2022-10-20 01:00:00 | US | 1327 |
|
||||
|
||||
| Time | Region | Value |
|
||||
| ------------------- | ------ | ----- |
|
||||
| 2022-10-20 12:00:00 | EU | 2936 |
|
||||
| 2022-10-20 01:00:00 | EU | 912 |
|
||||
|
||||
## Reporting: Zoom in and out to fit your data better into a PDF
|
||||
|
||||
Generally available in Grafana Enterprise, Cloud Pro, and Cloud Advanced.
|
||||
|
||||
Because dashboards appear on a screen and reports are PDFs, it can be challenging to render data just the way you want to. Sometimes the report doesn't show enough columns in a table, or the titles appear too small. Now you can adjust the scale of your report to zoom in and make each text field and panel larger or zoom out to show more data.
|
||||
|
||||
The zoom feature is located in the **Format Report** section of your reporting configuration. To learn more about reporting, refer to [Create and manage reports]({{< relref "../dashboards/create-reports/">}}).
|
||||
|
||||
{{< figure src="/static/img/docs/enterprise/reports/report-zoom.png" max-width="750px" caption="Report zoom feature with PDF documents at three different zoom levels" >}}
|
||||
|
||||
## Users and access
|
||||
|
||||
We've made the following improvements to users and access.
|
||||
|
||||
### OAuth: token handling improvements
|
||||
|
||||
Generally available in all editions of Grafana
|
||||
|
||||
As part of our efforts to improve the security of Grafana, we are introducing a long-awaited feature that enhances Grafana's OAuth 2.0 compatibility. When a user logs in using an OAuth provider, Grafana verifies on each request that the user's access token has not expired. Grafana uses the refresh token provided (if any exists) when an access token expires to obtain a new access token.
|
||||
|
||||
Because this feature introduces a breaking change, it is behind the `accessTokenExpirationCheck` feature toggle and is disabled by default. Enabling this functionality without configuring refresh tokens for the specific OAuth provider will sign users out after their access token has expired, and they would need to sign in again every time.
|
||||
|
||||
Complete documentation on how to configure obtaining a refresh token can be found on the [authentication configuration page]({{< relref "../setup-grafana/configure-security/configure-authentication/" >}}), in the instructions for your Oauth identity provider.
|
||||
|
||||
### Resolve user conflicts in Grafana's CLI
|
||||
|
||||
In the older versions of Grafana, usernames were case-sensitive. This created conflicts, where a user might sign in using two different methods (like SAML and OAuth) and have two accounts created, like `elastigirl@incredibles.com` and `ElastiGirl@incredibles.com`. Users in this situation might think they have lost their preferences and permissions. If this has occurred in your Grafana instance, you can use a new Grafana CLI command to resolve user identity conflicts between users within Grafana.
|
||||
|
||||
> Note: If you use Grafana Cloud or you run Grafana with MySQL as your database, you will not experience any user identity conflicts and you do not need to use this tool.
|
||||
|
||||
```bash
|
||||
# lists all the conflicting users
|
||||
$ grafana-cli user-manager conflicts list
|
||||
|
||||
# creates a conflict patch file to edit
|
||||
$ grafana-cli user-manager conflicts generate-file
|
||||
|
||||
# reads edited conflict patch file for validation
|
||||
$ grafana-cli user-manager conflicts validate-file <filepath>
|
||||
|
||||
# ingests the conflict users file. Can be executed once per file and will change the state of the database.
|
||||
$ grafana-cli user-manager conflicts ingest-file <filepath>
|
||||
```
|
||||
|
||||
### LDAP: Role mapping improvements
|
||||
|
||||
Generally available in all editions of Grafana
|
||||
|
||||
If you use an LDAP directory to authenticate to Grafana but prefer to assign organizations and roles in the Grafana UI
|
||||
or via API, you can now skip user organization role synchronization with your LDAP
|
||||
directory.
|
||||
|
||||
Use the `skip_org_role_sync` [LDAP authentication configuration option]({{< relref
|
||||
"../setup-grafana/configure-security/configure-authentication/ldap/#disable-org-role-synchronization" >}})
|
||||
when configuring LDAP authentication to prevent the synchronization between your LDAP groups and organization roles
|
||||
and make user roles editable manually.
|
||||
|
||||
### Azure AD OAuth2: New option to always fetch groups from the Graph API
|
||||
|
||||
Generally available in all editions of Grafana
|
||||
|
||||
If you use Azure AD OAuth2 authentication and use `SecurityEnabled` groups that you don't want Azure to embed in the
|
||||
authentication token, you can configure Grafana to use Microsoft's Graph API instead.
|
||||
|
||||
Use the [`force_use_graph_api` configuration option]({{< relref
|
||||
"../setup-grafana/configure-security/configure-authentication/azuread/#force-fetching-groups-from-microsoft-graph-api" >}})
|
||||
when configuring Azure AD authentication to force Grafana to fetch groups using Graph API.
|
||||
|
||||
### RBAC: List token's permissions
|
||||
|
||||
Generally available in Grafana Enterprise and Cloud Advanced
|
||||
|
||||
We added a new endpoint to help users diagnose permissions-related issues with user and token authorization.
|
||||
[This endpoint]({{< relref "../developers/http_api/access_control/#list-your-permissions" >}}) allows users to get the
|
||||
full list of RBAC permissions associated with their token.
|
||||
|
||||
For more details, refer to [Debug the permissions of a service account token]({{< relref
|
||||
"../administration/service-accounts/#debug-the-permissions-of-a-service-account-token" >}}).
|
||||
|
||||
### RBAC with Terraform: Extended support for provisioning permissions
|
||||
|
||||
Generally available in Grafana Enterprise and Cloud Advanced
|
||||
|
||||
All Grafana users can now use the latest release of [Terraform's Grafana provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) (version 1.31.1+) to provision [user and team access to service accounts]({{< relref "../administration/service-accounts/#manage-users-and-teams-permissions-for-a-service-account-in-grafana" >}}).
|
||||
|
||||
This allows full management of service accounts through Terraform - from creating a service account and allowing users to access it to assigning roles to the service account and generating service account tokens.
|
||||
|
||||
Grafana Enterprise and Cloud Pro and Advanced users can now provision [access to data sources]({{< relref "../administration/data-source-management/#data-source-permissions" >}}) for Grafana's `Viewer`, `Editor`, and `Admin` basic roles, as well as assign `Edit` permission.
|
||||
|
||||
We have also added [documentation on provisioning RBAC roles and role assignments]({{< relref "../administration/roles-and-permissions/access-control/rbac-terraform-provisioning/" >}}) to guide our Grafana Enterprise and Cloud Pro and Advanced users through this process.
|
||||
|
||||
Finally, we have fixed several access control related bugs to ensure a smoother provisioning experience.
|
||||
|
||||
## Alerting
|
||||
|
||||
All of these new alerting features are generally available in all editions of Grafana.
|
||||
|
||||
### Email templating
|
||||
|
||||
We've improved the design and functionality of email templates to make template creation much easier and more customizable. The email template framework utilizes MJML to define and compile the final email HTML output. Sprig functions in the email templates provide more customizable template functions.
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/alert-templates-whats-new-v9.3.png" max-width="750px" caption="Email template redesign" >}}
|
||||
|
||||
### Support for Webex Teams
|
||||
|
||||
You can now use Cisco Webex Teams as a contact point, to send alerts to a Webex Teams channel.
|
||||
|
||||
### Edit alert rules created using the provisioning API
|
||||
|
||||
Edit API-provisioned alert rules from the Grafana UI. To make a provisioned alert editable, add the `x-disable-provenance` header to the following requests when creating or editing your alert rules in the API:
|
||||
|
||||
POST /api/v1/provisioning/alert-rules
|
||||
|
||||
PUT /api/v1/provisioning/alert-rules/{UID}
|
||||
|
||||
### Support values in notification templates
|
||||
|
||||
Add alert values to notification templates, so that you can create a single template that prints the annotations, labels, and values for your alerts in a format of your choice.
|
||||
|
||||
### View notification errors
|
||||
|
||||
When an alert fails to fire, see when something is wrong with your contact point(s) and the reason for the error. The Receivers API contains information on the error, including a time stamp, duration of the attempt, and the error. You can also view the errors for each contact point in the UI.
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/alert-view-notification-errors-whats-new-v9.3.png" max-width="750px" caption="Alert notification errors" >}}
|
||||
|
||||
### Redesign of the expressions pipeline
|
||||
|
||||
We've redesigned the expressions pipeline editor to combine the expressions editor and the preview into a single view.
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/alert-expression-pipeline-whats-new-v9.3.png" max-width="750px" caption="Expression pipeline redesign" >}}
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
. scripts/grafana-server/variables
|
||||
|
||||
./e2e/run-suite verify/specs
|
||||
# The run-suite script requires a second argument to determine if videos should be recorded
|
||||
./e2e/run-suite verify/specs true
|
||||
|
||||
6
go.mod
6
go.mod
@@ -29,7 +29,7 @@ require (
|
||||
github.com/BurntSushi/toml v1.1.0
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
|
||||
github.com/aws/aws-sdk-go v1.44.109
|
||||
github.com/aws/aws-sdk-go v1.44.146
|
||||
github.com/beevik/etree v1.1.0
|
||||
github.com/benbjohnson/clock v1.3.0
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||
@@ -108,7 +108,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
|
||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
@@ -230,7 +230,7 @@ require (
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.uber.org/atomic v1.9.0
|
||||
go.uber.org/goleak v1.1.12 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
||||
13
go.sum
13
go.sum
@@ -368,8 +368,9 @@ github.com/aws/aws-sdk-go v1.38.60/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z
|
||||
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.40.37/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.109 h1:+Na5JPeS0kiEHoBp5Umcuuf+IDqXqD0lXnM920E31YI=
|
||||
github.com/aws/aws-sdk-go v1.44.109/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go v1.44.146 h1:7YdGgPxDPRJu/yYffzZp/H7yHzQ6AqmuNFZPYraaN8I=
|
||||
github.com/aws/aws-sdk-go v1.44.146/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.2 h1:fqlCk6Iy3bnCumtrLz9r3mJ/2gUT0pJ0wLFVIdWh+JA=
|
||||
@@ -2851,8 +2852,8 @@ golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
|
||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -3073,14 +3074,16 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "9.4.0-pre"
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "9.3.0"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"repository": "github:grafana/grafana",
|
||||
"scripts": {
|
||||
"build": "yarn i18n:compile && NODE_ENV=production webpack --config scripts/webpack/webpack.prod.js",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
@@ -30,11 +30,13 @@
|
||||
"scripts": {
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts",
|
||||
"clean": "rimraf ./dist ./compiled ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.1",
|
||||
"@grafana/schema": "9.4.0-pre",
|
||||
"@grafana/schema": "9.3.0",
|
||||
"@types/d3-interpolate": "^1.4.0",
|
||||
"d3-interpolate": "1.4.0",
|
||||
"date-fns": "2.29.3",
|
||||
|
||||
@@ -101,6 +101,26 @@ describe('field convert type', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can convert strings with commas to numbers', () => {
|
||||
const options = { targetField: 'stringy nums', destinationType: FieldType.number };
|
||||
|
||||
const stringyNumbers = {
|
||||
name: 'stringy nums',
|
||||
type: FieldType.string,
|
||||
values: new ArrayVector(['1,000', '1,000,000']),
|
||||
config: {},
|
||||
};
|
||||
|
||||
const numbers = convertFieldType(stringyNumbers, options);
|
||||
|
||||
expect(numbers).toEqual({
|
||||
name: 'stringy nums',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([1000, 1000000]),
|
||||
config: {},
|
||||
});
|
||||
});
|
||||
|
||||
describe('field convert types transformer', () => {
|
||||
beforeAll(() => {
|
||||
mockTransformationsRegistry([convertFieldTypeTransformer]);
|
||||
|
||||
@@ -142,7 +142,9 @@ function fieldToNumberField(field: Field): Field {
|
||||
const numValues = field.values.toArray().slice();
|
||||
|
||||
for (let n = 0; n < numValues.length; n++) {
|
||||
const number = +numValues[n];
|
||||
// some numbers returned from datasources have commas
|
||||
// strip the commas, coerce the string to a number
|
||||
const number = +numValues[n].replace(/,/g, '');
|
||||
numValues[n] = Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface FeatureToggles {
|
||||
objectStore?: boolean;
|
||||
traceqlEditor?: boolean;
|
||||
flameGraph?: boolean;
|
||||
cloudWatchCrossAccountQuerying?: boolean;
|
||||
redshiftAsyncQueryDataSupport?: boolean;
|
||||
athenaAsyncQueryDataSupport?: boolean;
|
||||
increaseInMemDatabaseQueryCache?: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e-selectors",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"description": "Grafana End-to-End Test Selectors Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -34,7 +34,9 @@
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts",
|
||||
"bundle": "rollup -c rollup.config.ts",
|
||||
"clean": "rimraf ./dist ./compiled ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "23.0.2",
|
||||
|
||||
@@ -3,7 +3,7 @@ const execa = require('execa');
|
||||
const { resolve, sep } = require('path');
|
||||
const resolveBin = require('resolve-as-bin');
|
||||
|
||||
const cypress = (commandName, { updateScreenshots }) => {
|
||||
const cypress = (commandName, { updateScreenshots, browser }) => {
|
||||
// Support running an unpublished dev build
|
||||
const dirname = __dirname.split(sep).pop();
|
||||
const projectPath = resolve(`${__dirname}${dirname === 'dist' ? '/..' : ''}`);
|
||||
@@ -16,6 +16,10 @@ const cypress = (commandName, { updateScreenshots }) => {
|
||||
|
||||
const cypressOptions = [commandName, '--env', `${CWD},${UPDATE_SCREENSHOTS}`, `--project=${projectPath}`];
|
||||
|
||||
if (browser) {
|
||||
cypressOptions.push('--browser', browser);
|
||||
}
|
||||
|
||||
const execaOptions = {
|
||||
cwd: __dirname,
|
||||
stdio: 'inherit',
|
||||
@@ -32,17 +36,21 @@ const cypress = (commandName, { updateScreenshots }) => {
|
||||
module.exports = () => {
|
||||
const updateOption = '-u, --update-screenshots';
|
||||
const updateDescription = 'update expected screenshots';
|
||||
const browserOption = '-b, --browser <browser>';
|
||||
const browserDescription = 'specify which browser to use';
|
||||
|
||||
program
|
||||
.command('open')
|
||||
.description('runs tests within the interactive GUI')
|
||||
.option(updateOption, updateDescription)
|
||||
.option(browserOption, browserDescription)
|
||||
.action((options) => cypress('open', options));
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.description('runs tests from the CLI without the GUI')
|
||||
.option(updateOption, updateDescription)
|
||||
.option(browserOption, browserDescription)
|
||||
.action((options) => cypress('run', options));
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -42,7 +42,9 @@
|
||||
"start": "cypress run --browser=chrome",
|
||||
"start-benchmark": "CYPRESS_NO_COMMAND_LOG=1 yarn start",
|
||||
"test": "pushd test && node ../dist/bin/grafana-e2e.js run",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "15.0.1",
|
||||
@@ -61,7 +63,7 @@
|
||||
"@babel/core": "7.19.6",
|
||||
"@babel/preset-env": "7.19.4",
|
||||
"@cypress/webpack-preprocessor": "5.15.2",
|
||||
"@grafana/e2e-selectors": "9.4.0-pre",
|
||||
"@grafana/e2e-selectors": "9.3.0",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"babel-loader": "9.1.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -32,13 +32,15 @@
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts",
|
||||
"bundle": "rollup -c rollup.config.ts",
|
||||
"clean": "rimraf ./dist ./compiled ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
"@grafana/e2e-selectors": "9.4.0-pre",
|
||||
"@grafana/data": "9.3.0",
|
||||
"@grafana/e2e-selectors": "9.3.0",
|
||||
"@grafana/faro-web-sdk": "1.0.0-beta2",
|
||||
"@grafana/ui": "9.4.0-pre",
|
||||
"@grafana/ui": "9.3.0",
|
||||
"@sentry/browser": "6.19.7",
|
||||
"history": "4.10.1",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -42,7 +42,8 @@ jest.mock('../services', () => ({
|
||||
|
||||
describe('DataSourceWithBackend', () => {
|
||||
test('check the executed queries', () => {
|
||||
const mock = runQueryAndReturnFetchMock({
|
||||
const { mock, ds } = createMockDatasource();
|
||||
ds.query({
|
||||
maxDataPoints: 10,
|
||||
intervalMs: 5000,
|
||||
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
|
||||
@@ -93,8 +94,22 @@ describe('DataSourceWithBackend', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
test('should apply template variables only for the current data source', () => {
|
||||
const { mock, ds } = createMockDatasource();
|
||||
ds.applyTemplateVariables = jest.fn();
|
||||
ds.query({
|
||||
maxDataPoints: 10,
|
||||
intervalMs: 5000,
|
||||
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
|
||||
} as DataQueryRequest);
|
||||
|
||||
expect(mock.calls.length).toBe(1);
|
||||
expect(ds.applyTemplateVariables).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('check that the executed queries is hidden from inspector', () => {
|
||||
const mock = runQueryAndReturnFetchMock({
|
||||
const { mock, ds } = createMockDatasource();
|
||||
ds.query({
|
||||
maxDataPoints: 10,
|
||||
intervalMs: 5000,
|
||||
targets: [{ refId: 'A' }, { refId: 'B', datasource: { type: 'sample' } }],
|
||||
@@ -169,9 +184,7 @@ describe('DataSourceWithBackend', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function runQueryAndReturnFetchMock(
|
||||
request: DataQueryRequest
|
||||
): jest.MockContext<Promise<FetchResponse>, BackendSrvRequest[]> {
|
||||
function createMockDatasource() {
|
||||
const settings = {
|
||||
name: 'test',
|
||||
id: 1234,
|
||||
@@ -184,7 +197,5 @@ function runQueryAndReturnFetchMock(
|
||||
mockDatasourceRequest.mockReturnValue(Promise.resolve({} as FetchResponse));
|
||||
|
||||
const ds = new MyDataSource(settings);
|
||||
ds.query(request);
|
||||
|
||||
return mockDatasourceRequest.mock;
|
||||
return { ds, mock: mockDatasourceRequest.mock };
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ class DataSourceWithBackend<
|
||||
const queries = targets.map((q) => {
|
||||
let datasource = this.getRef();
|
||||
let datasourceId = this.id;
|
||||
let shouldApplyTemplateVariables = true;
|
||||
|
||||
if (isExpressionReference(q.datasource)) {
|
||||
hasExpr = true;
|
||||
@@ -149,8 +150,15 @@ class DataSourceWithBackend<
|
||||
throw new Error(`Unknown Datasource: ${JSON.stringify(q.datasource)}`);
|
||||
}
|
||||
|
||||
datasource = ds.rawRef ?? getDataSourceRef(ds);
|
||||
datasourceId = ds.id;
|
||||
const dsRef = ds.rawRef ?? getDataSourceRef(ds);
|
||||
const dsId = ds.id;
|
||||
if (dsRef.uid !== datasource.uid || datasourceId !== dsId) {
|
||||
datasource = dsRef;
|
||||
datasourceId = dsId;
|
||||
// If the query is using a different datasource, we would need to retrieve the datasource
|
||||
// instance (async) and apply the template variables but it seems it's not necessary for now.
|
||||
shouldApplyTemplateVariables = false;
|
||||
}
|
||||
}
|
||||
if (datasource.type?.length) {
|
||||
pluginIDs.add(datasource.type);
|
||||
@@ -159,7 +167,7 @@ class DataSourceWithBackend<
|
||||
dsUIDs.add(datasource.uid);
|
||||
}
|
||||
return {
|
||||
...this.applyTemplateVariables(q, request.scopedVars),
|
||||
...(shouldApplyTemplateVariables ? this.applyTemplateVariables(q, request.scopedVars) : q),
|
||||
datasource,
|
||||
datasourceId, // deprecated!
|
||||
intervalMs,
|
||||
@@ -291,7 +299,7 @@ class DataSourceWithBackend<
|
||||
const result = await lastValueFrom(
|
||||
getBackendSrv().fetch<T>({
|
||||
...options,
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
headers: options?.headers ? { ...options.headers, ...headers } : headers,
|
||||
data: data ?? { ...data },
|
||||
url: `/api/datasources/${this.id}/resources/${path}`,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/schema",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"description": "Grafana Schema Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
@@ -31,7 +31,9 @@
|
||||
"build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts",
|
||||
"bundle": "rollup -c rollup.config.ts",
|
||||
"clean": "rimraf ./dist ./compiled ./package.tgz",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit"
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -51,10 +51,10 @@
|
||||
"@babel/preset-env": "7.18.9",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
"@grafana/data": "9.3.0",
|
||||
"@grafana/eslint-config": "5.0.0",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@grafana/ui": "9.4.0-pre",
|
||||
"@grafana/ui": "9.3.0",
|
||||
"@jest/core": "27.5.1",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/eslint": "8.4.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/ui",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -38,7 +38,9 @@
|
||||
"storybook": "start-storybook -p 9001 -c .storybook",
|
||||
"storybook:build": "build-storybook -o ./dist/storybook -c .storybook",
|
||||
"typecheck": "tsc --emitDeclarationOnly false --noEmit",
|
||||
"generate-icons-bundle-cache-file": "node ./scripts/generate-icon-bundle.js"
|
||||
"generate-icons-bundle-cache-file": "node ./scripts/generate-icon-bundle.js",
|
||||
"prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js",
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults",
|
||||
@@ -47,9 +49,9 @@
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.10.5",
|
||||
"@emotion/react": "11.10.5",
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
"@grafana/e2e-selectors": "9.4.0-pre",
|
||||
"@grafana/schema": "9.4.0-pre",
|
||||
"@grafana/data": "9.3.0",
|
||||
"@grafana/e2e-selectors": "9.3.0",
|
||||
"@grafana/schema": "9.3.0",
|
||||
"@leeoniya/ufuzzy": "0.8.0",
|
||||
"@monaco-editor/react": "4.4.6",
|
||||
"@popperjs/core": "2.11.6",
|
||||
|
||||
@@ -54,7 +54,7 @@ const getStyles = (theme: GrafanaTheme2, color: BadgeColor) => {
|
||||
} else {
|
||||
bgColor = tinycolor(sourceColor).setAlpha(0.15).toString();
|
||||
borderColor = tinycolor(sourceColor).lighten(20).toString();
|
||||
textColor = tinycolor(sourceColor).darken(15).toString();
|
||||
textColor = tinycolor(sourceColor).darken(20).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { Trans } from '../../../src/utils/i18n';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Button, ButtonProps } from '../Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
@@ -62,7 +63,7 @@ export function ClipboardButton({
|
||||
<>
|
||||
{showCopySuccess && (
|
||||
<InlineToast placement="top" referenceElement={buttonRef.current}>
|
||||
Copied
|
||||
<Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans>
|
||||
</InlineToast>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { FunctionComponent, useState } from 'react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { clearButtonStyles } from '../Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
@@ -71,9 +72,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
label: collapse__header;
|
||||
padding: ${theme.spacing(1, 2, 1, 2)};
|
||||
display: flex;
|
||||
cursor: inherit;
|
||||
transition: all 0.1s linear;
|
||||
cursor: pointer;
|
||||
`,
|
||||
headerCollapsed: css`
|
||||
label: collapse__header--collapsed;
|
||||
@@ -132,6 +131,7 @@ export const Collapse = ({
|
||||
className,
|
||||
children,
|
||||
}: React.PropsWithChildren<Props>) => {
|
||||
const buttonStyles = useStyles2(clearButtonStyles);
|
||||
const style = useStyles2(getStyles);
|
||||
const onClickToggle = () => {
|
||||
if (onToggle) {
|
||||
@@ -145,10 +145,10 @@ export const Collapse = ({
|
||||
|
||||
return (
|
||||
<div className={panelClass}>
|
||||
<div className={headerClass} onClick={onClickToggle}>
|
||||
<button type="button" className={cx(buttonStyles, headerClass)} onClick={onClickToggle}>
|
||||
{collapsible && <Icon className={style.icon} name={isOpen ? 'angle-down' : 'angle-right'} />}
|
||||
<div className={cx([style.headerLabel])}>{label}</div>
|
||||
</div>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className={cx([style.collapseBody])}>
|
||||
<div className={loaderClass} />
|
||||
|
||||
@@ -46,6 +46,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
|
||||
body,
|
||||
description,
|
||||
confirmText,
|
||||
confirmButtonVariant,
|
||||
dismissText,
|
||||
icon,
|
||||
isOpen,
|
||||
@@ -58,6 +59,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
|
||||
body={body}
|
||||
description={description}
|
||||
confirmText={confirmText}
|
||||
confirmButtonVariant={confirmButtonVariant}
|
||||
dismissText={dismissText}
|
||||
icon={icon}
|
||||
onConfirm={onConfirm}
|
||||
@@ -77,6 +79,7 @@ Basic.args = {
|
||||
body: 'Are you sure you want to delete this user?',
|
||||
description: 'Removing the user will not remove any dashboards the user has created',
|
||||
confirmText: 'Delete',
|
||||
confirmButtonVariant: 'destructive',
|
||||
dismissText: 'Cancel',
|
||||
icon: 'exclamation-triangle',
|
||||
isOpen: true,
|
||||
@@ -112,7 +115,7 @@ export const AlternativeAction: ComponentStory<typeof ConfirmModal> = ({
|
||||
|
||||
AlternativeAction.parameters = {
|
||||
controls: {
|
||||
exclude: [...defaultExcludes, 'confirmationText'],
|
||||
exclude: [...defaultExcludes, 'confirmationText', 'confirmButtonVariant'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -155,7 +158,7 @@ export const WithConfirmation: ComponentStory<typeof ConfirmModal> = ({
|
||||
|
||||
WithConfirmation.parameters = {
|
||||
controls: {
|
||||
exclude: [...defaultExcludes, 'alternativeText'],
|
||||
exclude: [...defaultExcludes, 'alternativeText', 'confirmButtonVariant'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { HorizontalGroup, Input } from '..';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { Button } from '../Button';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
|
||||
export interface ConfirmModalProps {
|
||||
@@ -31,6 +31,8 @@ export interface ConfirmModalProps {
|
||||
confirmationText?: string;
|
||||
/** Text for alternative button */
|
||||
alternativeText?: string;
|
||||
/** Confirm button variant */
|
||||
confirmButtonVariant?: ButtonVariant;
|
||||
/** Confirm action callback */
|
||||
onConfirm(): void;
|
||||
/** Dismiss action callback */
|
||||
@@ -53,6 +55,7 @@ export const ConfirmModal = ({
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
onAlternative,
|
||||
confirmButtonVariant = 'destructive',
|
||||
}: ConfirmModalProps): JSX.Element => {
|
||||
const [disabled, setDisabled] = useState(Boolean(confirmationText));
|
||||
const styles = useStyles2(getStyles);
|
||||
@@ -86,7 +89,7 @@ export const ConfirmModal = ({
|
||||
{dismissText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant={confirmButtonVariant}
|
||||
onClick={onConfirm}
|
||||
disabled={disabled}
|
||||
ref={buttonRef}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TooltipPlacement } from '../Tooltip/types';
|
||||
export interface Props {
|
||||
overlay: React.ReactElement | (() => React.ReactElement);
|
||||
placement?: TooltipPlacement;
|
||||
children: React.ReactElement;
|
||||
children: React.ReactElement | ((isOpen: boolean) => React.ReactElement);
|
||||
}
|
||||
|
||||
export const Dropdown = React.memo(({ children, overlay, placement }: Props) => {
|
||||
@@ -38,7 +38,7 @@ export const Dropdown = React.memo(({ children, overlay, placement }: Props) =>
|
||||
|
||||
return (
|
||||
<>
|
||||
{React.cloneElement(children, {
|
||||
{React.cloneElement(typeof children === 'function' ? children(visible) : children, {
|
||||
ref: setTriggerRef,
|
||||
})}
|
||||
{visible && (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { IconName } from '../../types';
|
||||
import { clearButtonStyles } from '../Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
export interface FilterPillProps {
|
||||
@@ -16,28 +17,28 @@ export interface FilterPillProps {
|
||||
|
||||
export const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick, icon = 'check' }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const clearButton = useStyles2(clearButtonStyles);
|
||||
return (
|
||||
<div className={cx(styles.wrapper, selected && styles.selected)} onClick={onClick}>
|
||||
<button type="button" className={cx(clearButton, styles.wrapper, selected && styles.selected)} onClick={onClick}>
|
||||
<span>{label}</span>
|
||||
{selected && <Icon name={icon} className={styles.icon} />}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrapper: css`
|
||||
padding: ${theme.spacing(0.25)} ${theme.spacing(1)};
|
||||
background: ${theme.colors.background.secondary};
|
||||
border-radius: ${theme.shape.borderRadius(8)};
|
||||
padding: ${theme.spacing(0, 2)};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
line-height: ${theme.typography.bodySmall.lineHeight};
|
||||
color: ${theme.colors.text.secondary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.action.hover};
|
||||
|
||||
@@ -19,6 +19,9 @@ const field: FieldConfig = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: [{ value: -Infinity, color: '#7EB26D' }],
|
||||
},
|
||||
custom: {
|
||||
neeutral: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const props: Props = {
|
||||
|
||||
@@ -98,6 +98,7 @@ export class Gauge extends PureComponent<Props> {
|
||||
gauge: {
|
||||
min,
|
||||
max,
|
||||
neutralValue: field.custom?.neutral,
|
||||
background: { color: backgroundColor },
|
||||
border: { color: null },
|
||||
shadow: { show: false },
|
||||
|
||||
41
packages/grafana-ui/src/components/Table/RowExpander.tsx
Normal file
41
packages/grafana-ui/src/components/Table/RowExpander.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Row } from 'react-table';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { getTableStyles } from './styles';
|
||||
|
||||
export interface Props {
|
||||
row: Row;
|
||||
expandedIndexes: Set<number>;
|
||||
setExpandedIndexes: (indexes: Set<number>) => void;
|
||||
}
|
||||
|
||||
export const RowExpander: FC<Props> = ({ row, expandedIndexes, setExpandedIndexes }) => {
|
||||
const tableStyles = useStyles2(getTableStyles);
|
||||
const isExpanded = expandedIndexes.has(row.index);
|
||||
// Use Cell to render an expander for each row.
|
||||
// We can use the getToggleRowExpandedProps prop-getter
|
||||
// to build the expander.
|
||||
return (
|
||||
<div
|
||||
className={tableStyles.expanderCell}
|
||||
onClick={() => {
|
||||
const newExpandedIndexes = new Set(expandedIndexes);
|
||||
if (isExpanded) {
|
||||
newExpandedIndexes.delete(row.index);
|
||||
} else {
|
||||
newExpandedIndexes.add(row.index);
|
||||
}
|
||||
setExpandedIndexes(newExpandedIndexes);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
aria-label={isExpanded ? 'Close trace' : 'Open trace'}
|
||||
name={isExpanded ? 'angle-down' : 'angle-right'}
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,14 @@ import { Table } from './Table';
|
||||
|
||||
Used for displaying tabular data
|
||||
|
||||
## Sub-tables
|
||||
|
||||
Sub-tables are supported through the usage of the prop `subData` Dataframe array.
|
||||
The frames are linked to each row using the following custom properties under `dataframe.meta.custom`
|
||||
|
||||
- **parentRowIndex**: number - The index of the parent row in the main dataframe (under the `data` prop of the Table component)
|
||||
- **noHeader**: boolean - Sets the noHeader of each sub-table
|
||||
|
||||
## Usage
|
||||
|
||||
<Props of={Table} />
|
||||
|
||||
@@ -36,7 +36,7 @@ const meta: ComponentMeta<typeof Table> = {
|
||||
args: {
|
||||
width: 700,
|
||||
height: 500,
|
||||
columnMinWidth: 150,
|
||||
columnMinWidth: 130,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -98,6 +98,61 @@ function buildData(theme: GrafanaTheme2, config: Record<string, FieldConfig>): D
|
||||
return prepDataForStorybook([data], theme)[0];
|
||||
}
|
||||
|
||||
function buildSubTablesData(theme: GrafanaTheme2, config: Record<string, FieldConfig>): DataFrame[] {
|
||||
const frames: DataFrame[] = [];
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const data = new MutableDataFrame({
|
||||
meta: {
|
||||
custom: {
|
||||
parentRowIndex: i,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [] }, // The time field
|
||||
{
|
||||
name: 'Quantity',
|
||||
type: FieldType.number,
|
||||
values: [],
|
||||
config: {
|
||||
decimals: 0,
|
||||
custom: {
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'Quality', type: FieldType.string, values: [] }, // The time field
|
||||
{
|
||||
name: 'Progress',
|
||||
type: FieldType.number,
|
||||
values: [],
|
||||
config: {
|
||||
unit: 'percent',
|
||||
min: 0,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
for (const field of data.fields) {
|
||||
field.config = merge(field.config, config[field.name]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < Math.random() * 4 + 1; i++) {
|
||||
data.appendRow([
|
||||
new Date().getTime(),
|
||||
Math.random() * 2,
|
||||
Math.random() > 0.7 ? 'Good' : 'Bad',
|
||||
Math.random() * 100,
|
||||
]);
|
||||
}
|
||||
|
||||
frames.push(data);
|
||||
}
|
||||
return prepDataForStorybook(frames, theme);
|
||||
}
|
||||
|
||||
function buildFooterData(data: DataFrame): FooterItem[] {
|
||||
const values = data.fields[3].values.toArray();
|
||||
const valueSum = values.reduce((prev, curr) => {
|
||||
@@ -195,4 +250,23 @@ Pagination.args = {
|
||||
enablePagination: true,
|
||||
};
|
||||
|
||||
export const SubTables: ComponentStory<typeof Table> = (args) => {
|
||||
const theme = useTheme2();
|
||||
const data = buildData(theme, {});
|
||||
const subData = buildSubTablesData(theme, {
|
||||
Progress: {
|
||||
custom: {
|
||||
displayMode: 'gradient-gauge',
|
||||
},
|
||||
thresholds: defaultThresholds,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="panel-container" style={{ width: 'auto', height: 'unset' }}>
|
||||
<Table {...args} data={data} subData={subData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -398,4 +398,54 @@ describe('Table', () => {
|
||||
expect(() => screen.getByTestId('table-footer')).toThrow('Unable to find an element');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when mounted with data and sub-data', () => {
|
||||
it('then correct rows should be rendered and new table is rendered when expander is clicked', () => {
|
||||
getTestContext({
|
||||
subData: new Array(getDefaultDataFrame().length).fill(0).map((i) =>
|
||||
toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'number' + i,
|
||||
type: FieldType.number,
|
||||
values: [i, i, i],
|
||||
config: {
|
||||
custom: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
custom: {
|
||||
parentRowIndex: i,
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
expect(getTable()).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('columnheader')).toHaveLength(4);
|
||||
expect(getColumnHeader(/time/)).toBeInTheDocument();
|
||||
expect(getColumnHeader(/temperature/)).toBeInTheDocument();
|
||||
expect(getColumnHeader(/img/)).toBeInTheDocument();
|
||||
|
||||
const rows = within(getTable()).getAllByRole('row');
|
||||
expect(rows).toHaveLength(5);
|
||||
expect(getRowsData(rows)).toEqual([
|
||||
{ time: '2021-01-01 00:00:00', temperature: '10', link: '10' },
|
||||
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: 'NaN' },
|
||||
{ time: '2021-01-01 01:00:00', temperature: '11', link: '11' },
|
||||
{ time: '2021-01-01 02:00:00', temperature: '12', link: '12' },
|
||||
]);
|
||||
|
||||
within(rows[1]).getByLabelText('Open trace').click();
|
||||
const rowsAfterClick = within(getTable()).getAllByRole('row');
|
||||
expect(within(rowsAfterClick[1]).getByRole('table')).toBeInTheDocument();
|
||||
expect(within(rowsAfterClick[1]).getByText(/number0/)).toBeInTheDocument();
|
||||
|
||||
expect(within(rowsAfterClick[2]).queryByRole('table')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
|
||||
import { DataFrame, getFieldDisplayName, Field } from '@grafana/data';
|
||||
|
||||
@@ -30,7 +31,14 @@ import {
|
||||
TableFooterCalc,
|
||||
GrafanaTableColumn,
|
||||
} from './types';
|
||||
import { getColumns, sortCaseInsensitive, sortNumber, getFooterItems, createFooterCalculationValues } from './utils';
|
||||
import {
|
||||
getColumns,
|
||||
sortCaseInsensitive,
|
||||
sortNumber,
|
||||
getFooterItems,
|
||||
createFooterCalculationValues,
|
||||
EXPANDER_WIDTH,
|
||||
} from './utils';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 150;
|
||||
|
||||
@@ -51,6 +59,8 @@ export interface Props {
|
||||
footerOptions?: TableFooterCalc;
|
||||
footerValues?: FooterItem[];
|
||||
enablePagination?: boolean;
|
||||
/** @alpha */
|
||||
subData?: DataFrame[];
|
||||
}
|
||||
|
||||
function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
|
||||
@@ -121,6 +131,7 @@ export const Table = memo((props: Props) => {
|
||||
const {
|
||||
ariaLabel,
|
||||
data,
|
||||
subData,
|
||||
height,
|
||||
onCellFilterAdded,
|
||||
width,
|
||||
@@ -134,13 +145,15 @@ export const Table = memo((props: Props) => {
|
||||
enablePagination,
|
||||
} = props;
|
||||
|
||||
const listRef = useRef<FixedSizeList>(null);
|
||||
const listRef = useRef<VariableSizeList>(null);
|
||||
const tableDivRef = useRef<HTMLDivElement>(null);
|
||||
const fixedSizeListScrollbarRef = useRef<HTMLDivElement>(null);
|
||||
const variableSizeListScrollbarRef = useRef<HTMLDivElement>(null);
|
||||
const tableStyles = useStyles2(getTableStyles);
|
||||
const theme = useTheme2();
|
||||
const headerHeight = noHeader ? 0 : tableStyles.cellHeight;
|
||||
const [footerItems, setFooterItems] = useState<FooterItem[] | undefined>(footerValues);
|
||||
const [expandedIndexes, setExpandedIndexes] = useState<Set<number>>(new Set());
|
||||
const prevExpandedIndexes = usePrevious(expandedIndexes);
|
||||
|
||||
const footerHeight = useMemo(() => {
|
||||
const EXTENDED_ROW_HEIGHT = 33;
|
||||
@@ -177,8 +190,8 @@ export const Table = memo((props: Props) => {
|
||||
|
||||
// React-table column definitions
|
||||
const memoizedColumns = useMemo(
|
||||
() => getColumns(data, width, columnMinWidth, footerItems),
|
||||
[data, width, columnMinWidth, footerItems]
|
||||
() => getColumns(data, width, columnMinWidth, expandedIndexes, setExpandedIndexes, !!subData?.length, footerItems),
|
||||
[data, width, columnMinWidth, footerItems, subData, expandedIndexes]
|
||||
);
|
||||
|
||||
// Internal react table state reducer
|
||||
@@ -260,14 +273,23 @@ export const Table = memo((props: Props) => {
|
||||
setPageSize(pageSize);
|
||||
}, [pageSize, setPageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
// react-table caches the height of cells so we need to reset them when expanding/collapsing rows
|
||||
// We need to take the minimum of the current expanded indexes and the previous expandedIndexes array to account
|
||||
// for collapsed rows, since they disappear from expandedIndexes but still keep their expanded height
|
||||
listRef.current?.resetAfterIndex(
|
||||
Math.min(...Array.from(expandedIndexes), ...(prevExpandedIndexes ? Array.from(prevExpandedIndexes) : []))
|
||||
);
|
||||
}, [expandedIndexes, prevExpandedIndexes]);
|
||||
|
||||
useEffect(() => {
|
||||
// To have the custom vertical scrollbar always visible (https://github.com/grafana/grafana/issues/52136),
|
||||
// we need to bring the element from the FixedSizeList scope to the outer Table container scope,
|
||||
// because the FixedSizeList scope has overflow. By moving scrollbar to container scope we will have
|
||||
// we need to bring the element from the VariableSizeList scope to the outer Table container scope,
|
||||
// because the VariableSizeList scope has overflow. By moving scrollbar to container scope we will have
|
||||
// it always visible since the entire width is in view.
|
||||
|
||||
// Select the scrollbar element from the FixedSizeList scope
|
||||
const listVerticalScrollbarHTML = (fixedSizeListScrollbarRef.current as HTMLDivElement)?.querySelector(
|
||||
// Select the scrollbar element from the VariableSizeList scope
|
||||
const listVerticalScrollbarHTML = (variableSizeListScrollbarRef.current as HTMLDivElement)?.querySelector(
|
||||
'.track-vertical'
|
||||
);
|
||||
|
||||
@@ -283,6 +305,40 @@ export const Table = memo((props: Props) => {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedIndexes(new Set());
|
||||
}, [data, subData]);
|
||||
|
||||
const renderSubTable = React.useCallback(
|
||||
(rowIndex: number) => {
|
||||
if (expandedIndexes.has(rowIndex)) {
|
||||
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === rowIndex);
|
||||
if (rowSubData) {
|
||||
const noHeader = !!rowSubData.meta?.custom?.noHeader;
|
||||
const subTableStyle: CSSProperties = {
|
||||
height: tableStyles.rowHeight * (rowSubData.length + (noHeader ? 0 : 1)), // account for the header with + 1
|
||||
background: theme.colors.emphasize(theme.colors.background.primary, 0.015),
|
||||
paddingLeft: EXPANDER_WIDTH,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
};
|
||||
return (
|
||||
<div style={subTableStyle}>
|
||||
<Table
|
||||
data={rowSubData}
|
||||
width={width - EXPANDER_WIDTH}
|
||||
height={tableStyles.rowHeight * (rowSubData.length + 1)}
|
||||
noHeader={noHeader}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[expandedIndexes, subData, tableStyles.rowHeight, theme.colors, width]
|
||||
);
|
||||
|
||||
const RenderRow = React.useCallback(
|
||||
({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
|
||||
let row = rows[rowIndex];
|
||||
@@ -290,8 +346,11 @@ export const Table = memo((props: Props) => {
|
||||
row = page[rowIndex];
|
||||
}
|
||||
prepareRow(row);
|
||||
|
||||
return (
|
||||
<div {...row.getRowProps({ style })} className={tableStyles.row}>
|
||||
{/*add the subtable to the DOM first to prevent a 1px border CSS issue on the last cell of the row*/}
|
||||
{renderSubTable(rowIndex)}
|
||||
{row.cells.map((cell: Cell, index: number) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
@@ -305,7 +364,7 @@ export const Table = memo((props: Props) => {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles]
|
||||
[onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles, renderSubTable]
|
||||
);
|
||||
|
||||
const onNavigate = useCallback(
|
||||
@@ -344,6 +403,17 @@ export const Table = memo((props: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const getItemSize = (index: number): number => {
|
||||
if (expandedIndexes.has(index)) {
|
||||
const rowSubData = subData?.find((frame) => frame.meta?.custom?.parentRowIndex === index);
|
||||
if (rowSubData) {
|
||||
const noHeader = !!rowSubData.meta?.custom?.noHeader;
|
||||
return tableStyles.rowHeight * (rowSubData.length + 1 + (noHeader ? 0 : 1)); // account for the header and the row data with + 1 + 1
|
||||
}
|
||||
}
|
||||
return tableStyles.rowHeight;
|
||||
};
|
||||
|
||||
const handleScroll: React.UIEventHandler = (event) => {
|
||||
const { scrollTop } = event.target as HTMLDivElement;
|
||||
|
||||
@@ -358,18 +428,18 @@ export const Table = memo((props: Props) => {
|
||||
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
|
||||
{!noHeader && <HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} />}
|
||||
{itemCount > 0 ? (
|
||||
<div ref={fixedSizeListScrollbarRef}>
|
||||
<div ref={variableSizeListScrollbarRef}>
|
||||
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true}>
|
||||
<FixedSizeList
|
||||
<VariableSizeList
|
||||
height={listHeight}
|
||||
itemCount={itemCount}
|
||||
itemSize={tableStyles.rowHeight}
|
||||
itemSize={getItemSize}
|
||||
width={'100%'}
|
||||
ref={listRef}
|
||||
style={{ overflow: undefined }}
|
||||
>
|
||||
{RenderRow}
|
||||
</FixedSizeList>
|
||||
</VariableSizeList>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -11,6 +11,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
const lineHeight = theme.typography.body.lineHeight;
|
||||
const bodyFontSize = 14;
|
||||
const cellHeight = cellPadding * 2 + bodyFontSize * lineHeight;
|
||||
const rowHeight = cellHeight + 2;
|
||||
const rowHoverBg = theme.colors.emphasize(theme.colors.background.primary, 0.03);
|
||||
|
||||
const buildCellContainerStyle = (color?: string, background?: string, overflowOnHover?: boolean) => {
|
||||
@@ -36,7 +37,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
label: ${overflowOnHover ? 'cellContainerOverflow' : 'cellContainerNoOverflow'};
|
||||
padding: ${cellPadding}px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: ${rowHeight}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-right: 1px solid ${borderColor};
|
||||
@@ -95,7 +96,7 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
buildCellContainerStyle,
|
||||
cellPadding,
|
||||
cellHeightInner: bodyFontSize * lineHeight,
|
||||
rowHeight: cellHeight + 2,
|
||||
rowHeight,
|
||||
table: css`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@@ -253,6 +254,13 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`,
|
||||
expanderCell: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: ${rowHeight}px;
|
||||
cursor: pointer;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Row } from 'react-table';
|
||||
|
||||
import { ArrayVector, Field, FieldType, MutableDataFrame, SelectableValue } from '@grafana/data';
|
||||
|
||||
import {
|
||||
@@ -44,21 +46,29 @@ function getData() {
|
||||
describe('Table utils', () => {
|
||||
describe('getColumns', () => {
|
||||
it('Should build columns from DataFrame', () => {
|
||||
const columns = getColumns(getData(), 1000, 120);
|
||||
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
|
||||
|
||||
expect(columns[0].Header).toBe('Time');
|
||||
expect(columns[1].Header).toBe('Value');
|
||||
});
|
||||
|
||||
it('Should distribute width and use field config width', () => {
|
||||
const columns = getColumns(getData(), 1000, 120);
|
||||
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
|
||||
|
||||
expect(columns[0].width).toBe(450);
|
||||
expect(columns[1].width).toBe(100);
|
||||
});
|
||||
|
||||
it('Should distribute width and use field config width with expander enabled', () => {
|
||||
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, true);
|
||||
|
||||
expect(columns[0].width).toBe(50); // expander column
|
||||
expect(columns[1].width).toBe(425);
|
||||
expect(columns[2].width).toBe(100);
|
||||
});
|
||||
|
||||
it('Should set field on columns', () => {
|
||||
const columns = getColumns(getData(), 1000, 120);
|
||||
const columns = getColumns(getData(), 1000, 120, new Set(), () => null, false);
|
||||
|
||||
expect(columns[0].field.name).toBe('Time');
|
||||
expect(columns[1].field.name).toBe('Value');
|
||||
@@ -82,8 +92,8 @@ describe('Table utils', () => {
|
||||
|
||||
describe('filterByValue', () => {
|
||||
describe('happy path', () => {
|
||||
const field: any = { values: new ArrayVector(['a', 'aa', 'ab', 'b', 'ba', 'bb', 'c']) };
|
||||
const rows: any = [
|
||||
const field = { values: new ArrayVector(['a', 'aa', 'ab', 'b', 'ba', 'bb', 'c']) } as unknown as Field;
|
||||
const rows = [
|
||||
{ index: 0, values: { 0: 'a' } },
|
||||
{ index: 1, values: { 0: 'aa' } },
|
||||
{ index: 2, values: { 0: 'ab' } },
|
||||
@@ -91,7 +101,7 @@ describe('Table utils', () => {
|
||||
{ index: 4, values: { 0: 'ba' } },
|
||||
{ index: 5, values: { 0: 'bb' } },
|
||||
{ index: 6, values: { 0: 'c' } },
|
||||
];
|
||||
] as unknown as Row[];
|
||||
const filterValues = [{ value: 'a' }, { value: 'b' }, { value: 'c' }];
|
||||
|
||||
const result = filterByValue(field)(rows, '0', filterValues);
|
||||
@@ -106,8 +116,8 @@ describe('Table utils', () => {
|
||||
describe('fast exit cases', () => {
|
||||
describe('no rows', () => {
|
||||
it('should return empty array', () => {
|
||||
const field: any = { values: new ArrayVector(['a']) };
|
||||
const rows: any = [];
|
||||
const field = { values: new ArrayVector(['a']) } as unknown as Field;
|
||||
const rows: Row[] = [];
|
||||
const filterValues = [{ value: 'a' }];
|
||||
|
||||
const result = filterByValue(field)(rows, '', filterValues);
|
||||
@@ -118,8 +128,8 @@ describe('Table utils', () => {
|
||||
|
||||
describe('no filterValues', () => {
|
||||
it('should return rows', () => {
|
||||
const field: any = { values: new ArrayVector(['a']) };
|
||||
const rows: any = [{}];
|
||||
const field = { values: new ArrayVector(['a']) } as unknown as Field;
|
||||
const rows = [{}] as Row[];
|
||||
const filterValues = undefined;
|
||||
|
||||
const result = filterByValue(field)(rows, '', filterValues);
|
||||
@@ -131,7 +141,7 @@ describe('Table utils', () => {
|
||||
describe('no field', () => {
|
||||
it('should return rows', () => {
|
||||
const field = undefined;
|
||||
const rows: any = [{}];
|
||||
const rows = [{}] as Row[];
|
||||
const filterValues = [{ value: 'a' }];
|
||||
|
||||
const result = filterByValue(field)(rows, '', filterValues);
|
||||
@@ -142,12 +152,12 @@ describe('Table utils', () => {
|
||||
|
||||
describe('missing id in values', () => {
|
||||
it('should return rows', () => {
|
||||
const field: any = { values: new ArrayVector(['a', 'b', 'c']) };
|
||||
const rows: any = [
|
||||
const field = { values: new ArrayVector(['a', 'b', 'c']) } as unknown as Field;
|
||||
const rows = [
|
||||
{ index: 0, values: { 0: 'a' } },
|
||||
{ index: 1, values: { 0: 'b' } },
|
||||
{ index: 2, values: { 0: 'c' } },
|
||||
];
|
||||
] as unknown as Row[];
|
||||
const filterValues = [{ value: 'a' }, { value: 'b' }, { value: 'c' }];
|
||||
|
||||
const result = filterByValue(field)(rows, '1', filterValues);
|
||||
@@ -188,7 +198,7 @@ describe('Table utils', () => {
|
||||
text: '1.0',
|
||||
}),
|
||||
};
|
||||
const rows: any[] = [];
|
||||
const rows = [] as Row[];
|
||||
|
||||
const result = calculateUniqueFieldValues(rows, field);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Property } from 'csstype';
|
||||
import { clone } from 'lodash';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React from 'react';
|
||||
import { Row } from 'react-table';
|
||||
|
||||
import {
|
||||
@@ -23,6 +24,7 @@ import { getFooterValue } from './FooterRow';
|
||||
import { GeoCell } from './GeoCell';
|
||||
import { ImageCell } from './ImageCell';
|
||||
import { JSONViewCell } from './JSONViewCell';
|
||||
import { RowExpander } from './RowExpander';
|
||||
import {
|
||||
CellComponent,
|
||||
TableCellDisplayMode,
|
||||
@@ -32,6 +34,8 @@ import {
|
||||
TableFooterCalc,
|
||||
} from './types';
|
||||
|
||||
export const EXPANDER_WIDTH = 50;
|
||||
|
||||
export function getTextAlign(field?: Field): Property.JustifyContent {
|
||||
if (!field) {
|
||||
return 'flex-start';
|
||||
@@ -61,11 +65,37 @@ export function getColumns(
|
||||
data: DataFrame,
|
||||
availableWidth: number,
|
||||
columnMinWidth: number,
|
||||
expandedIndexes: Set<number>,
|
||||
setExpandedIndexes: (indexes: Set<number>) => void,
|
||||
expander: boolean,
|
||||
footerValues?: FooterItem[]
|
||||
): GrafanaTableColumn[] {
|
||||
const columns: GrafanaTableColumn[] = [];
|
||||
const columns: GrafanaTableColumn[] = expander
|
||||
? [
|
||||
{
|
||||
// Make an expander cell
|
||||
Header: () => null, // No header
|
||||
id: 'expander', // It needs an ID
|
||||
Cell: ({ row }) => {
|
||||
return <RowExpander row={row} expandedIndexes={expandedIndexes} setExpandedIndexes={setExpandedIndexes} />;
|
||||
},
|
||||
width: EXPANDER_WIDTH,
|
||||
minWidth: EXPANDER_WIDTH,
|
||||
filter: (rows: Row[], id: string, filterValues?: SelectableValue[]) => {
|
||||
return [];
|
||||
},
|
||||
justifyContent: 'left',
|
||||
field: data.fields[0],
|
||||
sortType: 'basic',
|
||||
},
|
||||
]
|
||||
: [];
|
||||
let fieldCountWithoutWidth = 0;
|
||||
|
||||
if (expander) {
|
||||
availableWidth -= EXPANDER_WIDTH;
|
||||
}
|
||||
|
||||
for (const [fieldIndex, field] of data.fields.entries()) {
|
||||
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;
|
||||
|
||||
@@ -65,8 +65,8 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
|
||||
});
|
||||
|
||||
const config: Options = {
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
width: Math.floor(this.props.width),
|
||||
height: Math.floor(this.props.height),
|
||||
...this.props.config.getConfig(),
|
||||
};
|
||||
|
||||
@@ -93,8 +93,8 @@ export class UPlotChart extends Component<PlotProps, UPlotChartState> {
|
||||
|
||||
if (!sameDims(prevProps, this.props)) {
|
||||
plot?.setSize({
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
width: Math.floor(this.props.width),
|
||||
height: Math.floor(this.props.height),
|
||||
});
|
||||
} else if (!sameConfig(prevProps, this.props)) {
|
||||
this.reinitPlot();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jaegertracing/jaeger-ui-components",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.3.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -31,10 +31,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.10.5",
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
"@grafana/e2e-selectors": "9.4.0-pre",
|
||||
"@grafana/runtime": "9.4.0-pre",
|
||||
"@grafana/ui": "9.4.0-pre",
|
||||
"@grafana/data": "9.3.0",
|
||||
"@grafana/e2e-selectors": "9.3.0",
|
||||
"@grafana/runtime": "9.3.0",
|
||||
"@grafana/ui": "9.3.0",
|
||||
"chance": "^1.0.10",
|
||||
"classnames": "^2.2.5",
|
||||
"combokeys": "^3.0.0",
|
||||
|
||||
@@ -215,10 +215,20 @@ export default class ListView extends React.Component<TListViewProps> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(prevProps: TListViewProps) {
|
||||
if (this._itemHolderElm) {
|
||||
this._scanItemHeights();
|
||||
}
|
||||
// When windowScroller is set to false, we can continue to handle scrollElement
|
||||
if (this.props.windowScroller) {
|
||||
return;
|
||||
}
|
||||
// check if the scrollElement changes and update its scroll listener
|
||||
if (prevProps.scrollElement !== this.props.scrollElement) {
|
||||
prevProps.scrollElement?.removeEventListener('scroll', this._onScroll);
|
||||
this._wrapperElm = this.props.scrollElement;
|
||||
this._wrapperElm?.addEventListener('scroll', this._onScroll);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||
@@ -214,7 +215,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
|
||||
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, &usertest.FakeUserService{}, sqlStore)
|
||||
loginService := &logintest.LoginServiceFake{}
|
||||
authenticator := &logintest.AuthenticatorFake{}
|
||||
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(), nil, featuremgmt.WithFeatures())
|
||||
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, nil, authenticator, usertest.NewUserServiceFake(), orgtest.NewOrgServiceFake(), nil, featuremgmt.WithFeatures(), nil)
|
||||
|
||||
return ctxHdlr
|
||||
}
|
||||
@@ -250,15 +251,16 @@ func (s *fakeRenderService) Init() error {
|
||||
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
|
||||
store := sqlstore.InitTestDB(t)
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Live: newTestLive(t, store),
|
||||
License: &licensing.OSSLicensingService{},
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
QuotaService: quotatest.New(false, nil),
|
||||
RouteRegister: routing.NewRouteRegister(),
|
||||
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||
searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()),
|
||||
ldapGroups: ldap.ProvideGroupsService(),
|
||||
Cfg: cfg,
|
||||
Live: newTestLive(t, store),
|
||||
License: &licensing.OSSLicensingService{},
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
QuotaService: quotatest.New(false, nil),
|
||||
RouteRegister: routing.NewRouteRegister(),
|
||||
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||
searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()),
|
||||
ldapGroups: ldap.ProvideGroupsService(),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
|
||||
@@ -470,7 +470,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
}
|
||||
|
||||
if liveerr != nil {
|
||||
hs.log.Warn("unable to broadcast save event", "uid", dashboard.Uid, "error", err)
|
||||
hs.log.Warn("unable to broadcast save event", "uid", dashboard.Uid, "error", liveerr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +478,12 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
|
||||
return apierrors.ToDashboardErrorResponse(ctx, hs.pluginStore, err)
|
||||
}
|
||||
|
||||
// Clear permission cache for the user who's created the dashboard, so that new permissions are fetched for their next call
|
||||
// Required for cases when caller wants to immediately interact with the newly created object
|
||||
if newDashboard && !hs.accesscontrolService.IsDisabled() {
|
||||
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
||||
}
|
||||
|
||||
// connect library panels for this dashboard after the dashboard is stored and has an ID
|
||||
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(ctx, c.SignedInUser, dashboard)
|
||||
if err != nil {
|
||||
|
||||
@@ -130,11 +130,6 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *models.ReqContext) response.Res
|
||||
|
||||
metrics.MApiDashboardSnapshotExternal.Inc()
|
||||
} else {
|
||||
if cmd.Dashboard.Get("id").MustInt64() == 0 {
|
||||
c.JSON(http.StatusBadRequest, "Creating a local snapshot requires a dashboard")
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.Key == "" {
|
||||
var err error
|
||||
cmd.Key, err = util.GetRandomString(32)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/registry/corekind"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||
@@ -1093,6 +1094,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
folderService: folderService,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
Kinds: corekind.NewBase(nil),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
@@ -1201,6 +1203,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
dashboardVersionService: fakeDashboardVersionService,
|
||||
Kinds: corekind.NewBase(nil),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
|
||||
@@ -396,6 +396,12 @@ func (hs *HTTPServer) AddDataSource(c *models.ReqContext) response.Response {
|
||||
return response.Error(500, "Failed to add datasource", err)
|
||||
}
|
||||
|
||||
// Clear permission cache for the user who's created the data source, so that new permissions are fetched for their next call
|
||||
// Required for cases when caller wants to immediately interact with the newly created object
|
||||
if !hs.AccessControl.IsDisabled() {
|
||||
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
||||
}
|
||||
|
||||
ds := hs.convertModelToDtos(c.Req.Context(), cmd.Result)
|
||||
return response.JSON(http.StatusOK, util.DynMap{
|
||||
"message": "Datasource added",
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/permissions"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -112,7 +114,9 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{
|
||||
expectedDatasource: &datasources.DataSource{},
|
||||
},
|
||||
Cfg: setting.NewCfg(),
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: acimpl.ProvideAccessControl(setting.NewCfg()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources")
|
||||
@@ -224,7 +228,9 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
DataSourcesService: &dataSourcesServiceMock{
|
||||
expectedDatasource: &datasources.DataSource{},
|
||||
},
|
||||
Cfg: setting.NewCfg(),
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: acimpl.ProvideAccessControl(setting.NewCfg()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, "/api/datasources/1234")
|
||||
|
||||
@@ -128,6 +128,12 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext) response.Response {
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
// Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call
|
||||
// Required for cases when caller wants to immediately interact with the newly created object
|
||||
if !hs.AccessControl.IsDisabled() {
|
||||
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
||||
}
|
||||
|
||||
g := guardian.New(c.Req.Context(), folder.ID, c.OrgID, c.SignedInUser)
|
||||
// TODO set ParentUID if nested folders are enabled
|
||||
return response.JSON(http.StatusOK, hs.newToFolderDto(c, g, folder))
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -242,10 +243,11 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st
|
||||
store := mockstore.NewSQLStoreMock()
|
||||
guardian.InitLegacyGuardian(store, dashSvc, teamSvc)
|
||||
hs := HTTPServer{
|
||||
AccessControl: acmock.New(),
|
||||
folderService: folderService,
|
||||
Cfg: setting.NewCfg(),
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
AccessControl: acmock.New(),
|
||||
folderService: folderService,
|
||||
Cfg: setting.NewCfg(),
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
|
||||
@@ -58,7 +58,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
|
||||
grafanaUpdateChecker: &updatechecker.GrafanaService{},
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
PluginSettings: pluginSettings.ProvideService(sqlStore, secretsService),
|
||||
SocialService: social.ProvideService(cfg),
|
||||
SocialService: social.ProvideService(cfg, features),
|
||||
}
|
||||
|
||||
m := web.New()
|
||||
|
||||
@@ -77,6 +77,11 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
||||
settings["isPublicDashboardView"] = true
|
||||
}
|
||||
|
||||
weekStart := ""
|
||||
if prefs.WeekStart != nil {
|
||||
weekStart = *prefs.WeekStart
|
||||
}
|
||||
|
||||
data := dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{
|
||||
Id: c.UserID,
|
||||
@@ -93,7 +98,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
LightTheme: prefs.Theme == lightName,
|
||||
Timezone: prefs.Timezone,
|
||||
WeekStart: prefs.WeekStart,
|
||||
WeekStart: weekStart,
|
||||
Locale: locale,
|
||||
HelpFlags1: c.HelpFlags1,
|
||||
HasEditPermissionInFolders: hasEditPerm,
|
||||
|
||||
@@ -97,9 +97,7 @@ func (hs *HTTPServer) OAuthLogin(ctx *models.ReqContext) {
|
||||
|
||||
code := ctx.Query("code")
|
||||
if code == "" {
|
||||
// FIXME: access_type is a Google OAuth2 specific thing, consider refactoring this and moving to google_oauth.go
|
||||
opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
|
||||
|
||||
var opts []oauth2.AuthCodeOption
|
||||
if provider.UsePKCE {
|
||||
ascii, pkce, err := genPKCECode()
|
||||
if err != nil {
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@@ -36,7 +36,7 @@ func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux {
|
||||
Cfg: cfg,
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
SQLStore: sqlStore,
|
||||
SocialService: social.ProvideService(cfg),
|
||||
SocialService: social.ProvideService(cfg, featuremgmt.WithFeatures()),
|
||||
HooksService: hooks.ProvideService(),
|
||||
SecretsService: fakes.NewFakeSecretsService(),
|
||||
}
|
||||
|
||||
@@ -82,12 +82,17 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team
|
||||
}
|
||||
}
|
||||
|
||||
weekStart := ""
|
||||
if preference.WeekStart != nil {
|
||||
weekStart = *preference.WeekStart
|
||||
}
|
||||
|
||||
dto := dtos.Prefs{
|
||||
Theme: preference.Theme,
|
||||
HomeDashboardID: preference.HomeDashboardID,
|
||||
HomeDashboardUID: dashboardUID,
|
||||
Timezone: preference.Timezone,
|
||||
WeekStart: preference.WeekStart,
|
||||
WeekStart: weekStart,
|
||||
}
|
||||
|
||||
if preference.JSONData != nil {
|
||||
|
||||
@@ -41,6 +41,12 @@ func (hs *HTTPServer) CreateTeam(c *models.ReqContext) response.Response {
|
||||
return response.Error(500, "Failed to create Team", err)
|
||||
}
|
||||
|
||||
// Clear permission cache for the user who's created the team, so that new permissions are fetched for their next call
|
||||
// Required for cases when caller wants to immediately interact with the newly created object
|
||||
if !hs.AccessControl.IsDisabled() {
|
||||
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
||||
}
|
||||
|
||||
if accessControlEnabled || (c.OrgRole == org.RoleEditor && hs.Cfg.EditorsCanAdmin) {
|
||||
// if the request is authenticated using API tokens
|
||||
// the SignedInUser is an empty struct therefore
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
"github.com/grafana/grafana/pkg/services/preference/preftest"
|
||||
@@ -213,6 +215,8 @@ func TestTeamAPIEndpoint_CreateTeam_RBAC(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.teamService = teamtest.NewFakeService()
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(setting.NewCfg())
|
||||
hs.accesscontrolService = actest.FakeService{}
|
||||
})
|
||||
|
||||
input := strings.NewReader(fmt.Sprintf(teamCmd, 1))
|
||||
|
||||
@@ -392,6 +392,7 @@ func executeFPM(options linuxPackageOptions, packageRoot, srcDir string) error {
|
||||
switch options.packageType {
|
||||
case packageTypeRpm:
|
||||
args = append(args, "-t", "rpm", "--rpm-posttrans", "packaging/rpm/control/posttrans")
|
||||
args = append(args, "--rpm-digest", "sha256")
|
||||
case packageTypeDeb:
|
||||
args = append(args, "-t", "deb", "--deb-no-default-config-files")
|
||||
default:
|
||||
|
||||
@@ -144,11 +144,12 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
|
||||
}
|
||||
|
||||
rn := &rawNode{
|
||||
Query: rawQueryProp,
|
||||
RefID: query.RefID,
|
||||
TimeRange: query.TimeRange,
|
||||
QueryType: query.QueryType,
|
||||
DataSource: query.DataSource,
|
||||
Query: rawQueryProp,
|
||||
RefID: query.RefID,
|
||||
TimeRange: query.TimeRange,
|
||||
QueryType: query.QueryType,
|
||||
DataSource: query.DataSource,
|
||||
QueryEnricher: query.QueryEnricher,
|
||||
}
|
||||
|
||||
var node Node
|
||||
|
||||
@@ -42,11 +42,12 @@ type baseNode struct {
|
||||
}
|
||||
|
||||
type rawNode struct {
|
||||
RefID string `json:"refId"`
|
||||
Query map[string]interface{}
|
||||
QueryType string
|
||||
TimeRange TimeRange
|
||||
DataSource *datasources.DataSource
|
||||
RefID string `json:"refId"`
|
||||
Query map[string]interface{}
|
||||
QueryType string
|
||||
TimeRange TimeRange
|
||||
DataSource *datasources.DataSource
|
||||
QueryEnricher QueryDataRequestEnricher
|
||||
}
|
||||
|
||||
func (rn *rawNode) GetCommandType() (c CommandType, err error) {
|
||||
@@ -139,8 +140,9 @@ const (
|
||||
// DSNode is a DPNode that holds a datasource request.
|
||||
type DSNode struct {
|
||||
baseNode
|
||||
query json.RawMessage
|
||||
datasource *datasources.DataSource
|
||||
query json.RawMessage
|
||||
datasource *datasources.DataSource
|
||||
queryEnricher QueryDataRequestEnricher
|
||||
|
||||
orgID int64
|
||||
queryType string
|
||||
@@ -169,14 +171,15 @@ func (s *Service) buildDSNode(dp *simple.DirectedGraph, rn *rawNode, req *Reques
|
||||
id: dp.NewNode().ID(),
|
||||
refID: rn.RefID,
|
||||
},
|
||||
orgID: req.OrgId,
|
||||
query: json.RawMessage(encodedQuery),
|
||||
queryType: rn.QueryType,
|
||||
intervalMS: defaultIntervalMS,
|
||||
maxDP: defaultMaxDP,
|
||||
timeRange: rn.TimeRange,
|
||||
request: *req,
|
||||
datasource: rn.DataSource,
|
||||
orgID: req.OrgId,
|
||||
query: json.RawMessage(encodedQuery),
|
||||
queryType: rn.QueryType,
|
||||
intervalMS: defaultIntervalMS,
|
||||
maxDP: defaultMaxDP,
|
||||
timeRange: rn.TimeRange,
|
||||
request: *req,
|
||||
datasource: rn.DataSource,
|
||||
queryEnricher: rn.QueryEnricher,
|
||||
}
|
||||
|
||||
var floatIntervalMS float64
|
||||
@@ -211,24 +214,29 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s
|
||||
OrgID: dn.orgID,
|
||||
DataSourceInstanceSettings: dsInstanceSettings,
|
||||
PluginID: dn.datasource.Type,
|
||||
User: dn.request.User,
|
||||
}
|
||||
|
||||
q := []backend.DataQuery{
|
||||
{
|
||||
RefID: dn.refID,
|
||||
MaxDataPoints: dn.maxDP,
|
||||
Interval: time.Duration(int64(time.Millisecond) * dn.intervalMS),
|
||||
JSON: dn.query,
|
||||
TimeRange: dn.timeRange.AbsoluteTime(now),
|
||||
QueryType: dn.queryType,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := s.dataService.QueryData(ctx, &backend.QueryDataRequest{
|
||||
req := &backend.QueryDataRequest{
|
||||
PluginContext: pc,
|
||||
Queries: q,
|
||||
Headers: dn.request.Headers,
|
||||
})
|
||||
Queries: []backend.DataQuery{
|
||||
{
|
||||
RefID: dn.refID,
|
||||
MaxDataPoints: dn.maxDP,
|
||||
Interval: time.Duration(int64(time.Millisecond) * dn.intervalMS),
|
||||
JSON: dn.query,
|
||||
TimeRange: dn.timeRange.AbsoluteTime(now),
|
||||
QueryType: dn.queryType,
|
||||
},
|
||||
},
|
||||
Headers: dn.request.Headers,
|
||||
}
|
||||
|
||||
if dn.queryEnricher != nil {
|
||||
ctx = dn.queryEnricher(ctx, req)
|
||||
}
|
||||
|
||||
resp, err := s.dataService.QueryData(ctx, req)
|
||||
if err != nil {
|
||||
return mathexp.Results{}, err
|
||||
}
|
||||
@@ -389,7 +397,7 @@ func extractNumberSet(frame *data.Frame) ([]mathexp.Number, error) {
|
||||
labels[key] = val.(string) // TODO check assertion / return error
|
||||
}
|
||||
|
||||
n := mathexp.NewNumber("", labels)
|
||||
n := mathexp.NewNumber(frame.Fields[numericField].Name, labels)
|
||||
|
||||
// The new value fields' configs gets pointed to the one in the original frame
|
||||
n.Frame.Fields[0].Config = frame.Fields[numericField].Config
|
||||
|
||||
@@ -35,14 +35,19 @@ type Request struct {
|
||||
Debug bool
|
||||
OrgId int64
|
||||
Queries []Query
|
||||
User *backend.User
|
||||
}
|
||||
|
||||
// QueryDataRequestEnricher function definition for enriching a backend.QueryDataRequest request.
|
||||
type QueryDataRequestEnricher func(ctx context.Context, req *backend.QueryDataRequest) context.Context
|
||||
|
||||
// Query is like plugins.DataSubQuery, but with a a time range, and only the UID
|
||||
// for the data source. Also interval is a time.Duration.
|
||||
type Query struct {
|
||||
RefID string
|
||||
TimeRange TimeRange
|
||||
DataSource *datasources.DataSource `json:"datasource"`
|
||||
QueryEnricher QueryDataRequestEnricher
|
||||
JSON json.RawMessage
|
||||
Interval time.Duration
|
||||
QueryType string
|
||||
|
||||
9
pkg/infra/serverlock/errors.go
Normal file
9
pkg/infra/serverlock/errors.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package serverlock
|
||||
|
||||
type ServerLockExistsError struct {
|
||||
actionName string
|
||||
}
|
||||
|
||||
func (e *ServerLockExistsError) Error() string {
|
||||
return "there is already a lock for this actionName: " + e.actionName
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package serverlock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -185,7 +184,7 @@ func (sl *ServerLockService) acquireForRelease(ctx context.Context, actionName s
|
||||
if len(lockRows) > 0 {
|
||||
result := lockRows[0]
|
||||
if sl.isLockWithinInterval(result, maxInterval) {
|
||||
return errors.New("there is already a lock for this actionName: " + actionName)
|
||||
return &ServerLockExistsError{actionName: actionName}
|
||||
} else {
|
||||
// lock has timeouted, so we update the timestamp
|
||||
result.LastExecution = time.Now().Unix()
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/errors"
|
||||
"github.com/grafana/grafana"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/thema"
|
||||
tload "github.com/grafana/thema/load"
|
||||
)
|
||||
|
||||
// CoreStructuredDeclParentPath is the path, relative to the repository root, where
|
||||
@@ -48,28 +46,14 @@ func loadpFrameworkOnce() {
|
||||
})
|
||||
}
|
||||
|
||||
var prefix = filepath.Join("/pkg", "kindsys")
|
||||
|
||||
func doLoadFrameworkCUE(ctx *cue.Context) (cue.Value, error) {
|
||||
var v cue.Value
|
||||
var err error
|
||||
|
||||
absolutePath := prefix
|
||||
if !filepath.IsAbs(absolutePath) {
|
||||
absolutePath, err = filepath.Abs(absolutePath)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
}
|
||||
|
||||
bi, err := tload.InstancesWithThema(grafana.CueSchemaFS, absolutePath)
|
||||
v, err := cuectx.BuildGrafanaInstance(ctx, filepath.Join("pkg", "kindsys"), "kindsys", nil)
|
||||
if err != nil {
|
||||
return v, err
|
||||
}
|
||||
v = ctx.BuildInstance(bi)
|
||||
|
||||
if err = v.Validate(cue.Concrete(false), cue.All()); err != nil {
|
||||
return cue.Value{}, fmt.Errorf("coremodel framework loaded cue.Value has err: %w", err)
|
||||
return cue.Value{}, fmt.Errorf("kindsys framework loaded cue.Value has err: %w", err)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
func trueBoolPtr() *bool {
|
||||
@@ -54,7 +56,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1234",
|
||||
@@ -93,7 +95,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1234",
|
||||
@@ -143,7 +145,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Only other roles",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -171,7 +173,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Editor", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1234",
|
||||
@@ -220,7 +222,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Grafana Admin but setting is disabled",
|
||||
fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false)},
|
||||
fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures())},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
PreferredUsername: "",
|
||||
@@ -242,7 +244,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
name: "Editor roles in claim and GrafanaAdminAssignment enabled",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false)},
|
||||
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
PreferredUsername: "",
|
||||
@@ -263,7 +265,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Grafana Admin and Editor roles in claim",
|
||||
fields: fields{SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false)},
|
||||
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
PreferredUsername: "",
|
||||
@@ -302,7 +304,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
fields: fields{
|
||||
allowedGroups: []string{"foo", "bar"},
|
||||
SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false),
|
||||
&oauth2.Config{}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -324,7 +326,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch groups when ClaimsNames and ClaimsSources is set",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
claims: &azureClaims{
|
||||
ID: "1",
|
||||
@@ -349,7 +351,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch groups when forceUseGraphAPI is set",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()),
|
||||
forceUseGraphAPI: true,
|
||||
},
|
||||
claims: &azureClaims{
|
||||
@@ -376,7 +378,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch empty role when strict attribute role is true and no match",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -392,7 +394,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch empty role when strict attribute role is true and no role claims returned",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false),
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()),
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -416,7 +418,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.fields.SocialBase == nil {
|
||||
s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false)
|
||||
s.SocialBase = newSocialBase("azuread", &oauth2.Config{}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures())
|
||||
}
|
||||
|
||||
key := []byte("secret")
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
type SocialGenericOAuth struct {
|
||||
@@ -509,3 +511,10 @@ func (s *SocialGenericOAuth) FetchOrganizations(client *http.Client) ([]string,
|
||||
|
||||
return logins, true
|
||||
}
|
||||
|
||||
func (s *SocialGenericOAuth) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
|
||||
opts = append(opts, oauth2.AccessTypeOffline)
|
||||
}
|
||||
return s.SocialBase.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
const testGHUserTeamsJSON = `[
|
||||
@@ -202,7 +204,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
|
||||
|
||||
s := &SocialGithub{
|
||||
SocialBase: newSocialBase("github", &oauth2.Config{},
|
||||
&OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false),
|
||||
&OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false, *featuremgmt.WithFeatures()),
|
||||
allowedOrganizations: []string{},
|
||||
apiUrl: server.URL + "/user",
|
||||
teamIds: []int{},
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
type SocialGoogle struct {
|
||||
@@ -44,3 +46,10 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
|
||||
Login: data.Email,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
if s.features.IsEnabled(featuremgmt.FlagAccessTokenExpirationCheck) {
|
||||
opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
|
||||
}
|
||||
return s.SocialBase.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -58,7 +59,7 @@ type OAuthInfo struct {
|
||||
UsePKCE bool
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *SocialService {
|
||||
ss := SocialService{
|
||||
cfg: cfg,
|
||||
oAuthProvider: make(map[string]*OAuthInfo),
|
||||
@@ -139,7 +140,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
// GitHub.
|
||||
if name == "github" {
|
||||
ss.socialMap["github"] = &SocialGithub{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
@@ -149,7 +150,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
// GitLab.
|
||||
if name == "gitlab" {
|
||||
ss.socialMap["gitlab"] = &SocialGitlab{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||
}
|
||||
@@ -158,7 +159,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
// Google.
|
||||
if name == "google" {
|
||||
ss.socialMap["google"] = &SocialGoogle{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
hostedDomain: info.HostedDomain,
|
||||
apiUrl: info.ApiUrl,
|
||||
}
|
||||
@@ -167,7 +168,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
// AzureAD.
|
||||
if name == "azuread" {
|
||||
ss.socialMap["azuread"] = &SocialAzureAD{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
|
||||
}
|
||||
@@ -176,7 +177,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
// Okta
|
||||
if name == "okta" {
|
||||
ss.socialMap["okta"] = &SocialOkta{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||
}
|
||||
@@ -185,7 +186,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
// Generic - Uses the same scheme as GitHub.
|
||||
if name == "generic_oauth" {
|
||||
ss.socialMap["generic_oauth"] = &SocialGenericOAuth{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
teamsUrl: info.TeamsUrl,
|
||||
emailAttributeName: info.EmailAttributeName,
|
||||
@@ -214,8 +215,7 @@ func ProvideService(cfg *setting.Cfg) *SocialService {
|
||||
}
|
||||
|
||||
ss.socialMap[grafanaCom] = &SocialGrafanaCom{
|
||||
SocialBase: newSocialBase(name, &config, info,
|
||||
cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync),
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
url: cfg.GrafanaComURL,
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
}
|
||||
@@ -262,6 +262,7 @@ type SocialBase struct {
|
||||
roleAttributeStrict bool
|
||||
autoAssignOrgRole string
|
||||
skipOrgRoleSync bool
|
||||
features featuremgmt.FeatureManager
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
@@ -296,6 +297,7 @@ func newSocialBase(name string,
|
||||
info *OAuthInfo,
|
||||
autoAssignOrgRole string,
|
||||
skipOrgRoleSync bool,
|
||||
features featuremgmt.FeatureManager,
|
||||
) *SocialBase {
|
||||
logger := log.New("oauth." + name)
|
||||
|
||||
@@ -309,6 +311,7 @@ func newSocialBase(name string,
|
||||
roleAttributePath: info.RoleAttributePath,
|
||||
roleAttributeStrict: info.RoleAttributeStrict,
|
||||
skipOrgRoleSync: skipOrgRoleSync,
|
||||
features: features,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -833,7 +833,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg, mockSQLStore *dbtest.Fake
|
||||
tracer := tracing.InitializeTracerForTest()
|
||||
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginService, userService, mockSQLStore)
|
||||
authenticator := &logintest.AuthenticatorFake{ExpectedUser: &user.User{}}
|
||||
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, apiKeyService, authenticator, userService, orgService, oauthTokenService, featuremgmt.WithFeatures(featuremgmt.FlagAccessTokenExpirationCheck))
|
||||
return contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, mockSQLStore, tracer, authProxy, loginService, apiKeyService, authenticator, userService, orgService, oauthTokenService, featuremgmt.WithFeatures(featuremgmt.FlagAccessTokenExpirationCheck), nil)
|
||||
}
|
||||
|
||||
type fakeRenderService struct {
|
||||
|
||||
@@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
@@ -11,7 +12,8 @@ import (
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrUserTokenNotFound = errors.New("user token not found")
|
||||
ErrUserTokenNotFound = errors.New("user token not found")
|
||||
ErrInvalidSessionToken = errors.New("invalid session token")
|
||||
)
|
||||
|
||||
// CreateTokenErr represents a token creation error; used in Enterprise
|
||||
@@ -33,7 +35,11 @@ type TokenExpiredError struct {
|
||||
TokenID int64
|
||||
}
|
||||
|
||||
func (e *TokenExpiredError) Error() string { return "user token expired" }
|
||||
func (e *TokenExpiredError) Unwrap() error { return ErrInvalidSessionToken }
|
||||
|
||||
func (e *TokenExpiredError) Error() string {
|
||||
return fmt.Sprintf("%s: user token expired", ErrInvalidSessionToken)
|
||||
}
|
||||
|
||||
type TokenRevokedError struct {
|
||||
UserID int64
|
||||
@@ -41,7 +47,11 @@ type TokenRevokedError struct {
|
||||
MaxConcurrentSessions int64
|
||||
}
|
||||
|
||||
func (e *TokenRevokedError) Error() string { return "user token revoked" }
|
||||
func (e *TokenRevokedError) Error() string {
|
||||
return fmt.Sprintf("%s: user token revoked", ErrInvalidSessionToken)
|
||||
}
|
||||
|
||||
func (e *TokenRevokedError) Unwrap() error { return ErrInvalidSessionToken }
|
||||
|
||||
// UserToken represents a user token
|
||||
type UserToken struct {
|
||||
|
||||
@@ -127,7 +127,7 @@ func (s *Server) init() error {
|
||||
}
|
||||
|
||||
login.ProvideService(s.HTTPServer.SQLStore, s.HTTPServer.Login, s.loginAttemptService, s.userService)
|
||||
social.ProvideService(s.cfg)
|
||||
social.ProvideService(s.cfg, s.HTTPServer.Features)
|
||||
|
||||
if err := s.roleRegistry.RegisterFixedRoles(s.context); err != nil {
|
||||
return err
|
||||
|
||||
@@ -26,6 +26,8 @@ type Service interface {
|
||||
registry.ProvidesUsageStats
|
||||
// GetUserPermissions returns user permissions with only action and scope fields set.
|
||||
GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error)
|
||||
// ClearUserPermissionCache removes the permission cache entry for the given user
|
||||
ClearUserPermissionCache(user *user.SignedInUser)
|
||||
// DeleteUserPermissions removes all permissions user has in org and all permission to that user
|
||||
// If orgID is set to 0 remove permissions from all orgs
|
||||
DeleteUserPermissions(ctx context.Context, orgID, userID int64) error
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user