Merge branch 'main' into renovate/reduxjs-toolkit-1.x
This commit is contained in:
+35
-98
@@ -4298,6 +4298,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/components/PluginDetailsBody.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/PluginDetailsPage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/SearchField.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@@ -4316,9 +4319,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
],
|
||||
"public/app/features/plugins/admin/pages/PluginDetails.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/state/actions.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
@@ -4556,10 +4556,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/features/scenes/core/SceneTimeRange.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/features/scenes/core/sceneGraph.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
@@ -5373,8 +5369,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"]
|
||||
@@ -6838,8 +6833,7 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/testdata/nodeGraphUtils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/testdata/runStreams.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -6935,11 +6929,12 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/barchart/bars.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"]
|
||||
],
|
||||
"public/app/plugins/panel/barchart/module.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@@ -6950,7 +6945,8 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/plugins/panel/barchart/utils.ts: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.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/panel/candlestick/CandlestickPanel.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@@ -7573,24 +7569,13 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/nodeGraph/usePanning.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
],
|
||||
"public/app/plugins/panel/nodeGraph/useZoom.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/nodeGraph/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/panel/piechart/PieChart.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
@@ -7603,25 +7588,16 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/stat/StatMigrations.test.ts: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"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/stat/StatMigrations.ts: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"]
|
||||
],
|
||||
"public/app/plugins/panel/stat/common.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/state-timeline/TimelineChart.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "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.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/state-timeline/migrations.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -7644,23 +7620,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/state-timeline/utils.test.ts: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"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[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.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/state-timeline/utils.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/table-old/column_options.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -8026,16 +7989,10 @@ exports[`better eslint`] = {
|
||||
"public/test/core/redux/reduxTester.ts: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, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "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.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/test/core/thunk/thunkTester.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -8049,43 +8006,32 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
||||
],
|
||||
"public/test/global-jquery-shim.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/test/helpers/convertToStoreState.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/test/helpers/createFetchResponse.ts: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.", "0"]
|
||||
],
|
||||
"public/test/helpers/getDashboardModel.ts: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.", "0"]
|
||||
],
|
||||
"public/test/helpers/initTemplateSrv.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/test/jest-setup.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[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"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/test/jest-shim.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[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"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[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.", "5"]
|
||||
],
|
||||
"public/test/lib/common.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/test/matchers/index.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
@@ -8109,10 +8055,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/test/matchers/utils.ts: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"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/test/mocks/angular.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -8129,16 +8072,11 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/test/mocks/datasource_srv.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/test/mocks/workers.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/test/specs/helpers.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -8150,14 +8088,13 @@ 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, "Do not use any type assertions.", "9"],
|
||||
[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.", "15"]
|
||||
]
|
||||
}`
|
||||
};
|
||||
|
||||
+4
-1
@@ -36,7 +36,10 @@ function countEslintErrors() {
|
||||
};
|
||||
|
||||
const isTestFile =
|
||||
filePath.endsWith('.test.tsx') || filePath.endsWith('.test.ts') || filePath.includes('__mocks__');
|
||||
filePath.endsWith('.test.tsx') ||
|
||||
filePath.endsWith('.test.ts') ||
|
||||
filePath.includes('__mocks__') ||
|
||||
filePath.includes('public/test/');
|
||||
|
||||
if (!isTestFile) {
|
||||
rules['@typescript-eslint/consistent-type-assertions'] = [
|
||||
|
||||
@@ -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
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -69,6 +69,8 @@ go.sum @grafana/backend-platform
|
||||
/pkg/tsdb/loki @grafana/observability-logs
|
||||
/pkg/tsdb/zipkin @grafana/observability-traces-and-profiling
|
||||
/pkg/tsdb/tempo @grafana/observability-traces-and-profiling
|
||||
/pkg/tsdb/phlare @grafana/observability-traces-and-profiling
|
||||
/pkg/tsdb/parca @grafana/observability-traces-and-profiling
|
||||
|
||||
# BI backend code
|
||||
/pkg/tsdb/mysql @grafana/grafana-bi-squad
|
||||
@@ -144,6 +146,7 @@ tsconfig.json @grafana/frontend-ops
|
||||
/public/app/features/geo/ @grafana/grafana-edge-squad
|
||||
/public/app/features/storage/ @grafana/grafana-edge-squad
|
||||
/public/app/features/live/ @grafana/grafana-edge-squad
|
||||
/public/app/features/query-library/ @grafana/grafana-edge-squad
|
||||
/public/app/features/explore/ @grafana/observability-experience-squad
|
||||
/public/app/features/plugins @grafana/plugins-platform-frontend
|
||||
/public/app/features/transformers/spatial @grafana/grafana-edge-squad
|
||||
@@ -154,6 +157,7 @@ tsconfig.json @grafana/frontend-ops
|
||||
/public/app/plugins/panel/logs @grafana/observability-logs
|
||||
/public/app/plugins/panel/nodeGraph @grafana/observability-traces-and-profiling
|
||||
/public/app/plugins/panel/traces @grafana/observability-traces-and-profiling
|
||||
/public/app/plugins/panel/flamegraph @grafana/observability-traces-and-profiling
|
||||
/public/app/plugins/panel/piechart @grafana/grafana-bi-squad
|
||||
/public/app/plugins/panel/state-timeline @grafana/grafana-bi-squad
|
||||
/public/app/plugins/panel/status-history @grafana/grafana-bi-squad
|
||||
@@ -196,6 +200,8 @@ lerna.json @grafana/frontend-ops
|
||||
/public/app/plugins/datasource/cloud-monitoring @grafana/cloud-provider-plugins
|
||||
/public/app/plugins/datasource/zipkin @grafana/observability-traces-and-profiling
|
||||
/public/app/plugins/datasource/tempo @grafana/observability-traces-and-profiling
|
||||
/public/app/plugins/datasource/phlare @grafana/observability-traces-and-profiling
|
||||
/public/app/plugins/datasource/parca @grafana/observability-traces-and-profiling
|
||||
/public/app/plugins/datasource/alertmanager @grafana/alerting-squad
|
||||
|
||||
# SSE - Server Side Expressions
|
||||
@@ -219,6 +225,7 @@ lerna.json @grafana/frontend-ops
|
||||
/pkg/services/teamguardian @grafana/grafana-authnz-team
|
||||
/pkg/services/serviceaccounts @grafana/grafana-authnz-team
|
||||
/pkg/services/loginattempt @grafana/grafana-authnz-team
|
||||
/pkg/services/authn @grafana/grafana-authnz-team
|
||||
|
||||
# Grafana Partnerships Team
|
||||
/pkg/infra/httpclient/httpclientprovider/sigv4_middleware.go @grafana/grafana-partnerships-team
|
||||
|
||||
@@ -155,6 +155,22 @@
|
||||
"url":"https://github.com/orgs/grafana/projects/221"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type":"label",
|
||||
"name":"datasource/Phlare",
|
||||
"action":"addToProject",
|
||||
"addToProject":{
|
||||
"url":"https://github.com/orgs/grafana/projects/221"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type":"label",
|
||||
"name":"datasource/Parca",
|
||||
"action":"addToProject",
|
||||
"addToProject":{
|
||||
"url":"https://github.com/orgs/grafana/projects/221"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type":"label",
|
||||
"name":"datasource/Elasticsearch",
|
||||
|
||||
@@ -66,8 +66,7 @@
|
||||
"scripts/build/**/*",
|
||||
"scripts/*.sh",
|
||||
"Makefile",
|
||||
"Dockerfile",
|
||||
"Dockerfile.ubuntu"
|
||||
"Dockerfile"
|
||||
],
|
||||
"action": "updateLabel",
|
||||
"addLabel": "type/build-packaging"
|
||||
|
||||
@@ -23,4 +23,4 @@ jobs:
|
||||
repository: grafana/grafana-enterprise
|
||||
event_type: oss-pull-request
|
||||
client_payload:
|
||||
'{"source_branch": "${{ github.head_ref }}", "target_branch": "${{ github.base_ref }}", "pr_number": "${{ github.event.number }}"}'
|
||||
'{"source_sha": "${{ github.event.pull_request.head.sha }}", "source_branch": "${{ github.head_ref }}", "target_branch": "${{ github.base_ref }}", "pr_number": "${{ github.event.number }}"}'
|
||||
|
||||
@@ -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"
|
||||
@@ -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 }}"
|
||||
@@ -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 }}"
|
||||
@@ -1,3 +1,68 @@
|
||||
<!-- 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)
|
||||
@@ -167,6 +232,24 @@ The following functions and classes related to logs are deprecated in the `grafa
|
||||
- **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.2.7 START -->
|
||||
|
||||
# 9.2.7 (2022-11-29)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Access Control:** Clear user's permission cache after resource creation. [#59318](https://github.com/grafana/grafana/pull/59318), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
|
||||
- **Azure Monitor:** Fix empty/errored responses for Logs variables. [#59240](https://github.com/grafana/grafana/pull/59240), [@andresmgot](https://github.com/andresmgot)
|
||||
- **Azure Monitor:** Fix resource picker selection for subresources. [#56392](https://github.com/grafana/grafana/pull/56392), [@andresmgot](https://github.com/andresmgot)
|
||||
- **Navigation:** Fix crash when Help is disabled. [#58919](https://github.com/grafana/grafana/pull/58919), [@lpskdl](https://github.com/lpskdl)
|
||||
- **PostgreSQL:** Fix missing CA field from configuration. [#59280](https://github.com/grafana/grafana/pull/59280), [@oscarkilhed](https://github.com/oscarkilhed)
|
||||
- **SQL Datasources:** Fix annotation migration. [#59438](https://github.com/grafana/grafana/pull/59438), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **SQL:** Fix code editor for SQL datasources. [#58116](https://github.com/grafana/grafana/pull/58116), [@zoltanbedi](https://github.com/zoltanbedi)
|
||||
- **SSE:** Make sure to forward headers, user and cookies/OAuth token. [#58897](https://github.com/grafana/grafana/pull/58897), [@kylebrandt](https://github.com/kylebrandt)
|
||||
- **TimeseriesPanel:** Preserve string fields for data link interpolation. [#58424](https://github.com/grafana/grafana/pull/58424), [@mdvictor](https://github.com/mdvictor)
|
||||
|
||||
<!-- 9.2.7 END -->
|
||||
|
||||
<!-- 9.2.6 START -->
|
||||
|
||||
# 9.2.6 (2022-11-22)
|
||||
|
||||
+113
-46
@@ -1,8 +1,17 @@
|
||||
FROM node:16-alpine3.15 as js-builder
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BASE_IMAGE=alpine:3.15
|
||||
ARG JS_IMAGE=node:16-alpine3.15
|
||||
ARG GO_IMAGE=golang:1.19.3-alpine3.15
|
||||
|
||||
ARG GO_SRC=go-builder
|
||||
ARG JS_SRC=js-builder
|
||||
|
||||
FROM ${JS_IMAGE} as js-builder
|
||||
|
||||
ENV NODE_OPTIONS=--max_old_space_size=8000
|
||||
|
||||
WORKDIR /grafana
|
||||
WORKDIR /tmp/grafana
|
||||
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
COPY .yarn .yarn
|
||||
@@ -13,82 +22,140 @@ RUN yarn install
|
||||
|
||||
COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js babel.config.json .linguirc ./
|
||||
COPY public public
|
||||
COPY tools tools
|
||||
COPY scripts scripts
|
||||
COPY emails emails
|
||||
|
||||
ENV NODE_ENV production
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:1.19.3-alpine3.15 as go-builder
|
||||
FROM ${GO_IMAGE} as go-builder
|
||||
|
||||
RUN apk add --no-cache gcc g++ make
|
||||
# Install build dependencies
|
||||
RUN if grep -i -q alpine /etc/issue; then \
|
||||
apk add --no-cache gcc g++ make; \
|
||||
fi
|
||||
|
||||
WORKDIR /grafana
|
||||
WORKDIR /tmp/grafana
|
||||
|
||||
COPY go.mod go.sum embed.go Makefile build.go package.json ./
|
||||
COPY go.* ./
|
||||
COPY .bingo .bingo
|
||||
|
||||
RUN go mod download && \
|
||||
go install github.com/bwplotka/bingo@latest && \
|
||||
bingo get
|
||||
|
||||
COPY embed.go Makefile build.go package.json ./
|
||||
COPY cue.mod cue.mod
|
||||
COPY kinds kinds
|
||||
COPY packages/grafana-schema packages/grafana-schema
|
||||
COPY public/app/plugins public/app/plugins
|
||||
COPY public/api-spec.json public/api-spec.json
|
||||
COPY pkg pkg
|
||||
COPY scripts scripts
|
||||
COPY cue.mod cue.mod
|
||||
COPY .bingo .bingo
|
||||
COPY conf conf
|
||||
|
||||
RUN go mod verify
|
||||
RUN make build-go
|
||||
|
||||
# Final stage
|
||||
FROM alpine:3.15.6
|
||||
FROM ${BASE_IMAGE} as tgz-builder
|
||||
|
||||
LABEL maintainer="Grafana team <hello@grafana.com>"
|
||||
WORKDIR /tmp/grafana
|
||||
|
||||
ARG GRAFANA_TGZ="grafana-latest.linux-x64-musl.tar.gz"
|
||||
|
||||
COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
|
||||
|
||||
# add -v to make tar print every file it extracts
|
||||
RUN tar x -z -f /tmp/grafana.tar.gz --strip-components=1
|
||||
|
||||
# helpers for COPY --from
|
||||
FROM ${GO_SRC} as go-src
|
||||
FROM ${JS_SRC} as js-src
|
||||
|
||||
# Final stage
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
LABEL maintainer="Grafana Labs <hello@grafana.com>"
|
||||
|
||||
ARG GF_UID="472"
|
||||
ARG GF_GID="0"
|
||||
|
||||
ENV PATH="/usr/share/grafana/bin:$PATH" \
|
||||
GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
|
||||
GF_PATHS_DATA="/var/lib/grafana" \
|
||||
GF_PATHS_HOME="/usr/share/grafana" \
|
||||
GF_PATHS_LOGS="/var/log/grafana" \
|
||||
GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
|
||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
|
||||
GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
|
||||
GF_PATHS_DATA="/var/lib/grafana" \
|
||||
GF_PATHS_HOME="/usr/share/grafana" \
|
||||
GF_PATHS_LOGS="/var/log/grafana" \
|
||||
GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
|
||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
RUN apk add --no-cache ca-certificates bash tzdata musl-utils
|
||||
RUN apk info -vv | sort
|
||||
# Install dependencies
|
||||
RUN if grep -i -q alpine /etc/issue; then \
|
||||
apk add --no-cache ca-certificates bash tzdata musl-utils && \
|
||||
apk info -vv | sort; \
|
||||
elif grep -i -q ubuntu /etc/issue; then \
|
||||
DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y ca-certificates curl tzdata && \
|
||||
apt-get autoremove -y && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
else \
|
||||
echo 'ERROR: Unsupported base image' && /bin/false; \
|
||||
fi
|
||||
|
||||
COPY conf ./conf
|
||||
# glibc support for alpine x86_64 only
|
||||
RUN if grep -i -q alpine /etc/issue && [ `arch` = "x86_64" ]; then \
|
||||
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk \
|
||||
-O /tmp/glibc-2.35-r0.apk && \
|
||||
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-bin-2.35-r0.apk \
|
||||
-O /tmp/glibc-bin-2.35-r0.apk && \
|
||||
apk add --no-cache --allow-untrusted /tmp/glibc-2.35-r0.apk /tmp/glibc-bin-2.35-r0.apk && \
|
||||
rm -f /lib64/ld-linux-x86-64.so.2 && \
|
||||
ln -s /usr/glibc-compat/lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 && \
|
||||
rm -f /tmp/glibc-2.35-r0.apk && \
|
||||
rm -f /tmp/glibc-bin-2.35-r0.apk && \
|
||||
rm -f /lib/ld-linux-x86-64.so.2 && \
|
||||
rm -f /etc/ld.so.cache; \
|
||||
fi
|
||||
|
||||
COPY --from=go-src /tmp/grafana/conf ./conf
|
||||
|
||||
RUN if [ ! $(getent group "$GF_GID") ]; then \
|
||||
addgroup -S -g $GF_GID grafana; \
|
||||
fi
|
||||
if grep -i -q alpine /etc/issue; then \
|
||||
addgroup -S -g $GF_GID grafana; \
|
||||
else \
|
||||
addgroup --system --gid $GF_GID grafana; \
|
||||
fi; \
|
||||
fi && \
|
||||
GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \
|
||||
mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
if grep -i -q alpine /etc/issue; then \
|
||||
adduser -S -u $GF_UID -G "$GF_GID_NAME" grafana; \
|
||||
else \
|
||||
adduser --system --uid $GF_UID --ingroup "$GF_GID_NAME" grafana; \
|
||||
fi && \
|
||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
||||
"$GF_PATHS_PROVISIONING/plugins" \
|
||||
"$GF_PATHS_PROVISIONING/access-control" \
|
||||
"$GF_PATHS_PROVISIONING/alerting" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp conf/sample.ini "$GF_PATHS_CONFIG" && \
|
||||
cp conf/ldap.toml /etc/grafana/ldap.toml && \
|
||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
RUN export GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \
|
||||
mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
adduser -S -u $GF_UID -G "$GF_GID_NAME" grafana && \
|
||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
||||
"$GF_PATHS_PROVISIONING/plugins" \
|
||||
"$GF_PATHS_PROVISIONING/access-control" \
|
||||
"$GF_PATHS_PROVISIONING/alerting" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
|
||||
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
|
||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
COPY --from=go-builder /grafana/bin/*/grafana-server /grafana/bin/*/grafana-cli /grafana/bin/*/grafana ./bin/
|
||||
COPY --from=js-builder /grafana/public ./public
|
||||
COPY --from=js-builder /grafana/tools ./tools
|
||||
COPY --from=go-src /tmp/grafana/bin/grafana* /tmp/grafana/bin/*/grafana* ./bin/
|
||||
COPY --from=js-src /tmp/grafana/public ./public
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
COPY ./packaging/docker/run.sh /run.sh
|
||||
ARG RUN_SH=./packaging/docker/run.sh
|
||||
|
||||
USER grafana
|
||||
COPY ${RUN_SH} /run.sh
|
||||
|
||||
USER "$GF_UID"
|
||||
ENTRYPOINT [ "/run.sh" ]
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
FROM node:16-alpine3.15 as js-builder
|
||||
|
||||
ENV NODE_OPTIONS=--max_old_space_size=8000
|
||||
|
||||
WORKDIR /usr/src/app/
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
COPY packages packages
|
||||
COPY .yarnrc.yml ./
|
||||
COPY .yarn .yarn
|
||||
COPY plugins-bundled plugins-bundled
|
||||
|
||||
RUN yarn install
|
||||
|
||||
COPY tsconfig.json .eslintrc .editorconfig .browserslistrc .prettierrc.js babel.config.json .linguirc ./
|
||||
COPY public public
|
||||
COPY tools tools
|
||||
COPY scripts scripts
|
||||
COPY emails emails
|
||||
|
||||
ENV NODE_ENV production
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:1.19.3 AS go-builder
|
||||
|
||||
WORKDIR /src/grafana
|
||||
|
||||
COPY go.mod go.sum embed.go ./
|
||||
COPY Makefile build.go package.json ./
|
||||
COPY .bingo .bingo
|
||||
COPY pkg pkg/
|
||||
COPY cue.mod cue.mod/
|
||||
COPY packages/grafana-schema packages/grafana-schema/
|
||||
COPY public/app/plugins public/app/plugins/
|
||||
COPY public/api-spec.json public/api-spec.json
|
||||
|
||||
RUN go mod verify
|
||||
RUN make build-go
|
||||
|
||||
FROM ubuntu:20.04
|
||||
|
||||
LABEL maintainer="Grafana team <hello@grafana.com>"
|
||||
EXPOSE 3000
|
||||
|
||||
ARG GF_UID="472"
|
||||
ARG GF_GID="472"
|
||||
|
||||
ENV PATH="/usr/share/grafana/bin:$PATH" \
|
||||
GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
|
||||
GF_PATHS_DATA="/var/lib/grafana" \
|
||||
GF_PATHS_HOME="/usr/share/grafana" \
|
||||
GF_PATHS_LOGS="/var/log/grafana" \
|
||||
GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
|
||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
COPY conf conf
|
||||
|
||||
# curl should be part of the image
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl
|
||||
|
||||
RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
addgroup --system --gid $GF_GID grafana && \
|
||||
adduser --uid $GF_UID --system --ingroup grafana grafana && \
|
||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
||||
"$GF_PATHS_PROVISIONING/plugins" \
|
||||
"$GF_PATHS_PROVISIONING/access-control" \
|
||||
"$GF_PATHS_PROVISIONING/alerting" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp conf/sample.ini "$GF_PATHS_CONFIG" && \
|
||||
cp conf/ldap.toml /etc/grafana/ldap.toml && \
|
||||
chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
COPY --from=go-builder /src/grafana/bin/*/grafana-server /src/grafana/bin/*/grafana-cli /grafana/bin/*/grafana bin/
|
||||
COPY --from=js-builder /usr/src/app/public public
|
||||
COPY --from=js-builder /usr/src/app/tools tools
|
||||
|
||||
COPY packaging/docker/run.sh /
|
||||
|
||||
USER grafana
|
||||
ENTRYPOINT [ "/run.sh" ]
|
||||
@@ -158,12 +158,17 @@ shellcheck: $(SH_FILES) ## Run checks for shell scripts.
|
||||
|
||||
build-docker-full: ## Build Docker image for development.
|
||||
@echo "build docker container"
|
||||
docker build --tag grafana/grafana:dev .
|
||||
DOCKER_BUILDKIT=1 \
|
||||
docker build \
|
||||
--tag grafana/grafana:dev .
|
||||
|
||||
build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development.
|
||||
@echo "build docker container"
|
||||
docker build --tag grafana/grafana:dev-ubuntu -f ./Dockerfile.ubuntu .
|
||||
|
||||
DOCKER_BUILDKIT=1 \
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=ubuntu:20.04 \
|
||||
--build-arg GO_IMAGE=golang:1.19.3 \
|
||||
--tag grafana/grafana:dev-ubuntu .
|
||||
|
||||
##@ Services
|
||||
|
||||
|
||||
@@ -85,6 +85,11 @@ cdn_url =
|
||||
# `0` means there is no timeout for reading the request.
|
||||
read_timeout = 0
|
||||
|
||||
# This setting enables you to specify additional headers that the server adds to HTTP(S) responses.
|
||||
[server.custom_response_headers]
|
||||
#exampleHeader1 = exampleValue1
|
||||
#exampleHeader2 = exampleValue2
|
||||
|
||||
#################################### Database ############################
|
||||
[database]
|
||||
# You can configure the database connection by specifying type, host, name, user and password
|
||||
@@ -712,6 +717,10 @@ managed_identity_client_id =
|
||||
# If enabled, cache permissions in a in memory cache
|
||||
permission_cache = true
|
||||
|
||||
# Reset basic roles permissions on boot
|
||||
# Warning left to true, basic roles permissions will be reset on every boot
|
||||
reset_basic_roles = false
|
||||
|
||||
#################################### SMTP / Emailing #####################
|
||||
[smtp]
|
||||
enabled = false
|
||||
@@ -1359,3 +1368,14 @@ index_update_interval = 10s
|
||||
# Move a specific app plugin page (referenced by its `path` field) to a specific navigation section
|
||||
# Format: <Page URL> = <Section ID> <Sort Weight>
|
||||
[navigation.app_standalone_pages]
|
||||
|
||||
|
||||
#################################### Secure Socks5 Datasource Proxy #####################################
|
||||
[secure_socks_datasource_proxy]
|
||||
enabled = false
|
||||
root_ca_cert =
|
||||
client_key =
|
||||
client_cert =
|
||||
server_name =
|
||||
# The address of the socks5 proxy datasources should connect to
|
||||
proxy_address =
|
||||
@@ -86,6 +86,11 @@
|
||||
# `0` means there is no timeout for reading the request.
|
||||
;read_timeout = 0
|
||||
|
||||
# This setting enables you to specify additional headers that the server adds to HTTP(S) responses.
|
||||
[server.custom_response_headers]
|
||||
#exampleHeader1 = exampleValue1
|
||||
#exampleHeader2 = exampleValue2
|
||||
|
||||
#################################### Database ####################################
|
||||
[database]
|
||||
# You can configure the database connection by specifying type, host, name, user and password
|
||||
@@ -698,6 +703,11 @@
|
||||
#################################### Role-based Access Control ###########
|
||||
[rbac]
|
||||
;permission_cache = true
|
||||
|
||||
# Reset basic roles permissions on boot
|
||||
# Warning left to true, basic roles permissions will be reset on every boot
|
||||
#reset_basic_roles = false
|
||||
|
||||
#################################### SMTP / Emailing ##########################
|
||||
[smtp]
|
||||
;enabled = false
|
||||
@@ -1285,3 +1295,13 @@
|
||||
[navigation.app_standalone_pages]
|
||||
# The following will move the page with the path "/a/my-app-id/starred-content" from `my-app-id` to the `starred` section
|
||||
# /a/my-app-id/starred-content = starred
|
||||
|
||||
#################################### Secure Socks5 Datasource Proxy #####################################
|
||||
[secure_socks_datasource_proxy]
|
||||
; enabled = false
|
||||
; root_ca_cert =
|
||||
; client_key =
|
||||
; client_cert =
|
||||
; server_name =
|
||||
# The address of the socks5 proxy datasources should connect to
|
||||
; proxy_address =
|
||||
@@ -0,0 +1,320 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 530,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "decmbytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 14,
|
||||
"w": 11,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"maxDataPoints": 30,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 0.97,
|
||||
"groupWidth": 0.7,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "vertical",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelMaxLength": 6,
|
||||
"xTickLabelRotation": 45,
|
||||
"xTickLabelSpacing": 100
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"label\",\n \"type\": \"string\",\n \"typeInfo\": {\n \"frame\": \"string\",\n \"nullable\": true\n },\n \"config\": {\n \"interval\": 600000\n }\n },\n {\n \"name\": \"A-series\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"float64\",\n \"nullable\": true\n },\n \"labels\": {},\n \"config\": {}\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n \"acquisition\",\n \"extension\",\n \"conductor\",\n \"authorise\",\n \"architect\",\n \"illusion\",\n \"congress\",\n \"highlight\",\n \"partnership\",\n \"understanding\",\n \"disagreement\",\n \"personality\",\n \"commerce\",\n \"systematic\",\n \"hesitate\",\n \"business\",\n \"manufacture\",\n \"incredible\",\n \"constitutional\",\n \"prevalence\",\n \"professor\",\n \"entitlement\",\n \"cooperation\",\n \"sickness\",\n \"contrast\",\n \"reference\",\n \"audience\",\n \"discount\",\n \"apparatus\",\n \"disturbance\",\n \"automatic\",\n \"refrigerator\",\n \"elaborate\",\n \"sympathetic\",\n \"integration\",\n \"president\"\n ],\n [\n 306.78931659492116,\n 200.00696051101917,\n 164.90889283973593,\n 518.9385023737021,\n 999.9040675564702,\n 613.9689830172349,\n 773.2337077340269,\n 317.47395634701644,\n 748.3318338316539,\n 606.8039493787173,\n 426.27771317792866,\n 376.47735643253924,\n 66.30635081800493,\n 401.70654338415505,\n 108.86259550477234,\n 182.40284186231278,\n 867.7047958572101,\n 959.3957783599242,\n 396.7606089549935,\n 455.9625595614323,\n 685.4792456298062,\n 368.6567303946707,\n 157.06596562976327,\n 59.54120602048763,\n 406.72723615743973,\n 440.18247585615575,\n 516.0267558264891,\n 258.76006051667315,\n 952.966531725171,\n 554.8746357628739,\n 86.7279280805682,\n 781.2422516386563,\n 754.2723802427706,\n 435.0305712850233,\n 384.43181614983,\n 459.04164596738127\n ]\n ]\n }\n }\n]",
|
||||
"refId": "A",
|
||||
"scenarioId": "raw_frame"
|
||||
}
|
||||
],
|
||||
"title": "Panel Title",
|
||||
"type": "barchart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"axisSoftMin": 0,
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"decimals": 7,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 14,
|
||||
"w": 13,
|
||||
"x": 11,
|
||||
"y": 0
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 1,
|
||||
"groupWidth": 0.82,
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"max"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"text": {},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelRotation": 45,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, -5\nLondon, 10, 1\nLong value, 15,10",
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Auto sizing & auto show values",
|
||||
"type": "barchart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "decmbytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 18,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 14
|
||||
},
|
||||
"id": 3,
|
||||
"maxDataPoints": 20,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 0.97,
|
||||
"groupWidth": 0.7,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showValue": "auto",
|
||||
"stacking": "none",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xTickLabelMaxLength": 5,
|
||||
"xTickLabelRotation": 45,
|
||||
"xTickLabelSpacing": 100
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Panel Title",
|
||||
"type": "barchart"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"barchart",
|
||||
"graph-ng"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "BarChart - Label Rotation & Skipping",
|
||||
"uid": "xCmMwXdVz",
|
||||
"version": 20,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -107,6 +107,13 @@ local dashboard = grafana.dashboard;
|
||||
id: 0,
|
||||
}
|
||||
},
|
||||
dashboard.new('barchart-label-rotation-skipping', import '../dev-dashboards/panel-barchart/barchart-label-rotation-skipping.json') +
|
||||
resource.addMetadata('folder', 'dev-dashboards') +
|
||||
{
|
||||
spec+: {
|
||||
id: 0,
|
||||
}
|
||||
},
|
||||
dashboard.new('barchart-thresholds-mappings', import '../dev-dashboards/panel-barchart/barchart-thresholds-mappings.json') +
|
||||
resource.addMetadata('folder', 'dev-dashboards') +
|
||||
{
|
||||
|
||||
@@ -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**.
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ The table below describes all RBAC configuration options. Like any other Grafana
|
||||
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `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` |
|
||||
| `reset_basic_roles` | No | Reset Grafana's basic roles' (Viewer, Editor, Admin, Grafana Admin) permissions to their default. Warning, if this configuration option is left to `true` this will be done on every reboot. | `true` |
|
||||
|
||||
## Example RBAC configuration
|
||||
|
||||
|
||||
+24
-2
@@ -309,7 +309,29 @@ You can also change basic roles' permissions using the API. Refer to the [RBAC H
|
||||
|
||||
## Reset basic roles to their default
|
||||
|
||||
This section describes how to reset the basic roles to their default:
|
||||
This section describes how to reset the basic roles to their default.
|
||||
|
||||
You have two options to reset the basic roles permissions to their default.
|
||||
|
||||
### Use the configuration option
|
||||
|
||||
> **Note**: Available as of Grafana Enterprise 9.4.
|
||||
|
||||
> Warning: If this option is left to true, permissions will be reset on every boot.
|
||||
|
||||
Use the [reset_basic_roles]({{< relref "../configure-rbac/#configure-rbac-in-grafana" >}}) option to reset
|
||||
basic roles permissions to their default on Grafana instance boot up.
|
||||
|
||||
1. Open you configuration file and update the rbac section as follow:
|
||||
|
||||
```bash
|
||||
[rbac]
|
||||
reset_basic_roles = true
|
||||
```
|
||||
|
||||
### Use the http endpoint
|
||||
|
||||
An alternative to the configuration option is to use the HTTP endpoint.
|
||||
|
||||
1. Open the YAML configuration file and locate the `roles` section.
|
||||
|
||||
@@ -327,7 +349,7 @@ This section describes how to reset the basic roles to their default:
|
||||
permissions:
|
||||
# Permission allowing to reset basic roles
|
||||
- action: 'roles:write'
|
||||
scope: 'permissions:type:escalate'
|
||||
scope: 'permissions:type:escalate'
|
||||
```
|
||||
|
||||
1. As a `Grafana Admin`, call the API endpoint to reset the basic roles to their default. Refer to the [RBAC HTTP API]({{< relref "../../../../developers/http_api/access_control/#reset-basic-roles-to-their-default" >}}) for more details.
|
||||
|
||||
@@ -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/)
|
||||
@@ -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,41 +71,50 @@ 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 effect.
|
||||
| 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
|
||||
|
||||
@@ -95,20 +128,3 @@ For example, if a screenshot could not be taken within the expected time (10 sec
|
||||
- `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.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If Grafana has been set up to send images in notifications, however notifications are still being received without images, then check the troubleshooting instructions below.
|
||||
|
||||
1. Check that Grafana has been set up as per the instructions
|
||||
2. Check that the notifier supports images in notifications as per the table of supported notifiers
|
||||
3. Enable debug logging in Grafana and look for logs with the logger `ngalert.image`
|
||||
4. 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`
|
||||
5. 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`
|
||||
6. 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
|
||||
|
||||
@@ -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" >}}
|
||||
|
||||
@@ -53,8 +53,6 @@ You can also configure settings specific to the Jaeger data source:
|
||||
The **Trace to logs** section configures the [trace to logs feature]({{< relref "../../explore/trace-integration/" >}}).
|
||||
Select a target data source, limited to Loki and Splunk \[logs\] data sources, and which tags to use in the logs query.
|
||||
|
||||
{{< figure src="/static/img/docs/explore/trace-to-log-8-2.png" class="docs-image--no-shadow" caption="Screenshot of the trace to logs settings" >}}
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Data source** | Sets the target data source. |
|
||||
|
||||
@@ -50,10 +50,6 @@ Set the data source's basic configuration options carefully:
|
||||
The **Trace to logs** section configures the [trace to logs feature]({{< relref "../../explore/trace-integration/" >}}).
|
||||
Select a target data source, limited to Loki and Splunk \[logs\] data sources, and which tags to use in the logs query.
|
||||
|
||||
<!--
|
||||
{{< figure src="/static/img/docs/explore/trace-to-log-8-2.png" class="docs-image--no-shadow" caption="Screenshot of the trace to logs settings" >}}
|
||||
-->
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Data source** | Sets the target data source. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -85,9 +85,27 @@ Click on the node and select "Show in Graph layout" option to switch back to gra
|
||||
|
||||
This visualization needs a specific shape of the data to be returned from the data source in order to correctly display it.
|
||||
|
||||
Data source needs to return two data frames, one for nodes and one for edges. You have to set `frame.meta.preferredVisualisationType = 'nodeGraph'` on both data frames or name them `nodes` and `edges` respectively for the node graph to render.
|
||||
Node Graph at minimum requires a data frame describing the edges of the graph. By default, node graph will compute the nodes and any stats based on this data frame. Optionally a second data frame describing the nodes can be sent in case there is need to show more node specific metadata. You have to set `frame.meta.preferredVisualisationType = 'nodeGraph'` on both data frames or name them `nodes` and `edges` respectively for the node graph to render.
|
||||
|
||||
### Node parameters
|
||||
### Edges data frame structure
|
||||
|
||||
Required fields:
|
||||
|
||||
| Field name | Type | Description |
|
||||
| ---------- | ------ | ------------------------------ |
|
||||
| id | string | Unique identifier of the edge. |
|
||||
| source | string | Id of the source node. |
|
||||
| target | string | Id of the target. |
|
||||
|
||||
Optional fields:
|
||||
|
||||
| Field name | Type | Description |
|
||||
| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| mainstat | string/number | First stat shown in the overlay when hovering over the edge. It can be a string showing the value as is or it can be a number. If it is a number, any unit associated with that field is also shown |
|
||||
| secondarystat | string/number | Same as mainStat, but shown right under it. |
|
||||
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. |
|
||||
|
||||
### Nodes data frame structure
|
||||
|
||||
Required fields:
|
||||
|
||||
@@ -105,21 +123,3 @@ Optional fields:
|
||||
| secondarystat | string/number | Same as mainStat, but shown under it inside the node. |
|
||||
| arc\_\_\* | number | Any field prefixed with `arc__` will be used to create the color circle around the node. All values in these fields should add up to 1. You can specify color using `config.color.fixedColor`. |
|
||||
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. |
|
||||
|
||||
### Edge parameters
|
||||
|
||||
Required fields:
|
||||
|
||||
| Field name | Type | Description |
|
||||
| ---------- | ------ | ------------------------------ |
|
||||
| id | string | Unique identifier of the edge. |
|
||||
| source | string | Id of the source node. |
|
||||
| target | string | Id of the target. |
|
||||
|
||||
Optional fields:
|
||||
|
||||
| Field name | Type | Description |
|
||||
| ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| mainstat | string/number | First stat shown in the overlay when hovering over the edge. It can be a string showing the value as is or it can be a number. If it is a number, any unit associated with that field is also shown |
|
||||
| secondarystat | string/number | Same as mainStat, but shown right under it. |
|
||||
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the edge. Use `config.displayName` for more human readable label. |
|
||||
|
||||
@@ -160,3 +160,18 @@ Columns with filters applied have a blue funnel displayed next to the title.
|
||||
{{< figure src="/static/img/docs/tables/filtered-column.png" max-width="500px" caption="Filtered column" class="docs-image--no-shadow" >}}
|
||||
|
||||
To remove the filter, click the blue funnel icon and then click **Clear filter**.
|
||||
|
||||
## Table footer
|
||||
|
||||
You can use the table footer to show [calculations]({{< relref "../../calculation-types/" >}}) on fields.
|
||||
|
||||
After you enable the table footer:
|
||||
|
||||
1. Select the **Calculation**
|
||||
2. Select the **Fields** that you want to calculate
|
||||
|
||||
The system applies the calculation to all numeric fields if you do not select a field.
|
||||
|
||||
### Count rows
|
||||
|
||||
If you want to show the number of rows in the dataset instead of the number of values in the selected fields, select the **Count** calculation and enable **Count rows**.
|
||||
|
||||
@@ -292,6 +292,17 @@ Sets the maximum time using a duration format (5s/5m/5ms) before timing out read
|
||||
|
||||
<hr />
|
||||
|
||||
## [server.custom_response_headers]
|
||||
|
||||
This setting enables you to specify additional headers that the server adds to HTTP(S) responses.
|
||||
|
||||
```
|
||||
exampleHeader1 = exampleValue1
|
||||
exampleHeader2 = exampleValue2
|
||||
```
|
||||
|
||||
<hr />
|
||||
|
||||
## [database]
|
||||
|
||||
Grafana needs a database to store users and dashboards (and other
|
||||
@@ -760,6 +771,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.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/
|
||||
description: Learn about toggles for experimental and beta features, which you can enable or disable.
|
||||
title: Configure feature toggles
|
||||
weight: 150
|
||||
---
|
||||
|
||||
<!-- DO NOT EDIT THIS PAGE, it is machine generated by running the test in -->
|
||||
<!-- https://github.com/grafana/grafana/blob/main/pkg/services/featuremgmt/toggles_gen_test.go#L19 -->
|
||||
|
||||
# Configure feature toggles
|
||||
|
||||
You use feature toggles, also known as feature flags, to turn experimental or beta features on and off in Grafana. Although we do not recommend using these features in production, you can turn on feature toggles to try out new functionality in development or test environments.
|
||||
|
||||
This page contains a list of available feature toggles. To learn how to turn on feature toggles, refer to our [Configure Grafana documentation]({{< relref "../_index.md/#feature_toggles" >}}). Feature toggles are also available to Grafana Cloud Advanced customers. If you use Grafana Cloud Advanced, you can open a support ticket and specify the feature toggles and stack for which you want them enabled.
|
||||
|
||||
## Stable feature toggles
|
||||
|
||||
Some stable features are enabled by default. You can disable a stable feature by setting the feature flag to "false" in the configuration.
|
||||
|
||||
| Feature toggle name | Description | Enabled by default |
|
||||
| ---------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| `promQueryBuilder` | Show Prometheus query builder | Yes |
|
||||
| `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | |
|
||||
| `database_metrics` | Add Prometheus metrics for database tables | |
|
||||
| `lokiMonacoEditor` | Access to Monaco query editor for Loki | Yes |
|
||||
| `featureHighlights` | Highlight Grafana Enterprise features | |
|
||||
| `commandPalette` | Enable command palette | Yes |
|
||||
| `cloudWatchDynamicLabels` | Use dynamic labels instead of alias patterns in CloudWatch datasource | Yes |
|
||||
| `prometheusBufferedClient` | Enable buffered (old) client for Prometheus datasource as default instead of streaming JSON parser client (new) | |
|
||||
| `internationalization` | Enables internationalization | |
|
||||
| `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | |
|
||||
|
||||
## Beta feature toggles
|
||||
|
||||
| Feature toggle name | Description |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `trimDefaults` | Use cue schema to remove values that will be applied automatically |
|
||||
| `panelTitleSearch` | Search for dashboards using panel title |
|
||||
| `prometheusAzureOverrideAudience` | Experimental. Allow override default AAD audience for Azure Prometheus endpoint |
|
||||
| `swaggerUi` | Serves swagger UI |
|
||||
| `migrationLocking` | Lock database during migrations |
|
||||
| `newDBLibrary` | Use jmoiron/sqlx rather than xorm for a few backend services |
|
||||
| `validateDashboardsOnSave` | Validate dashboard JSON POSTed to api/dashboards/db |
|
||||
| `autoMigrateGraphPanels` | Replace the angular graph panel with timeseries |
|
||||
| `interFont` | Switch to inter font |
|
||||
| `datasourceLogger` | Logs all datasource requests |
|
||||
|
||||
## Alpha feature toggles
|
||||
|
||||
These features are early in their development lifecycle and so are not yet supported in Grafana Cloud.
|
||||
Alpha features might be changed or removed without prior notice.
|
||||
|
||||
| Feature toggle name | Description |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `alertingBigTransactions` | Use big transactions for alerting database writes |
|
||||
| `dashboardPreviews` | Create and show thumbnails for dashboard search results |
|
||||
| `live-config` | Save Grafana Live configuration in SQL tables |
|
||||
| `live-pipeline` | Enable a generic live processing pipeline |
|
||||
| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread |
|
||||
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
|
||||
| `tempoApmTable` | Show APM table |
|
||||
| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy |
|
||||
| `publicDashboards` | Enables public access to dashboards |
|
||||
| `lokiLive` | Support WebSocket streaming for loki (early prototype) |
|
||||
| `lokiDataframeApi` | Use experimental loki api for WebSocket streaming (early prototype) |
|
||||
| `dashboardComments` | Enable dashboard-wide comments |
|
||||
| `annotationComments` | Enable annotation comments |
|
||||
| `storage` | Configurable storage for dashboards, datasources, and resources |
|
||||
| `exploreMixedDatasource` | Enable mixed datasource in Explore |
|
||||
| `tracing` | Adds trace ID to error notifications |
|
||||
| `correlations` | Correlations page |
|
||||
| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query |
|
||||
| `traceToMetrics` | Enable trace to metrics links |
|
||||
| `prometheusWideSeries` | Enable wide series responses in the Prometheus datasource |
|
||||
| `canvasPanelNesting` | Allow elements nesting |
|
||||
| `scenes` | Experimental framework to build interactive dashboards |
|
||||
| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables |
|
||||
| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown |
|
||||
| `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. |
|
||||
| `topnav` | New top nav and page layouts |
|
||||
| `traceqlEditor` | Show the TraceQL editor in the explore page |
|
||||
| `flameGraph` | Show the flame graph |
|
||||
| `cloudWatchCrossAccountQuerying` | Use cross-account querying in CloudWatch datasource |
|
||||
| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift |
|
||||
| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena |
|
||||
| `increaseInMemDatabaseQueryCache` | Enable more in memory caching for database queries |
|
||||
| `newPanelChromeUI` | Show updated look and feel of grafana-ui PanelChrome: panel header, icons, and menu |
|
||||
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
|
||||
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
|
||||
| `elasticsearchBackendMigration` | Use Elasticsearch as backend data source |
|
||||
| `secureSocksDatasourceProxy` | Enable secure socks tunneling for supported core datasources |
|
||||
| `authnService` | Use new auth service to perform authentication |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
The following toggles require explicitly setting Grafana's [app mode]({{< relref "../_index.md/#app_mode" >}}) to 'development' before you can enable this feature toggle. These features tend to be experimental.
|
||||
|
||||
| Feature toggle name | Description |
|
||||
| -------------------------------------- | --------------------------------------------------------- |
|
||||
| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI |
|
||||
| `showFeatureFlagsInUI` | Show feature flags in the settings UI |
|
||||
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
|
||||
| `export` | Export grafana instance (to git, etc) |
|
||||
| `azureMonitorResourcePickerForMetrics` | New UI for Azure Monitor Metrics Query |
|
||||
| `grpcServer` | Run GRPC server |
|
||||
| `entityStore` | SQL-based entity store (requires storage flag also) |
|
||||
| `queryLibrary` | Reusable query library |
|
||||
| `accessControlOnCall` | Access control primitives for OnCall |
|
||||
| `nestedFolders` | Enable folder nesting |
|
||||
@@ -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/" >}})
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
_build:
|
||||
list: false
|
||||
aliases:
|
||||
- /docs/grafana/latest/guides/whats-new-in-v9-2/
|
||||
description: Feature and improvement highlights for Grafana v9.2
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
---
|
||||
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" >}}
|
||||
@@ -0,0 +1,159 @@
|
||||
'use strict';
|
||||
|
||||
const Mocha = require('mocha');
|
||||
const { EVENT_TEST_END, EVENT_RUN_END, EVENT_TEST_FAIL, EVENT_TEST_PASS } = Mocha.Runner.constants;
|
||||
|
||||
class LogReporter extends Mocha.reporters.Base {
|
||||
constructor(runner, options = {}) {
|
||||
super(runner, options);
|
||||
|
||||
this.tests = [];
|
||||
this.failures = [];
|
||||
this.passes = [];
|
||||
|
||||
runner.on(EVENT_TEST_END, (test) => {
|
||||
this.tests.push(cleanTest(test));
|
||||
});
|
||||
|
||||
runner.on(EVENT_TEST_PASS, (test) => {
|
||||
this.passes.push(cleanTest(test));
|
||||
});
|
||||
|
||||
runner.on(EVENT_TEST_FAIL, (test) => {
|
||||
this.failures.push(cleanTest(test));
|
||||
});
|
||||
|
||||
runner.once(EVENT_RUN_END, () => {
|
||||
this.reportStats();
|
||||
this.reportResults();
|
||||
this.reportErrors();
|
||||
});
|
||||
}
|
||||
|
||||
reportStats() {
|
||||
const stats = {
|
||||
...this.stats,
|
||||
// Format time in epoch
|
||||
start: this.stats.start.getTime(),
|
||||
end: this.stats.end.getTime(),
|
||||
};
|
||||
|
||||
// Example
|
||||
// CypressStats suites=1 tests=2 passes=1 pending=0 failures=1
|
||||
// start=1668783563731 end=1668783645198 duration=81467
|
||||
console.log(`CypressStats ${objToLogAttributes(stats)}`);
|
||||
}
|
||||
|
||||
reportResults() {
|
||||
this.tests.map((test) => {
|
||||
// Example
|
||||
// CypressTestResult title="Login scenario, create test data source, dashboard, panel, and export scenario"
|
||||
// suite="Smoke tests" file=../../e2e/smoke-tests-suite/1-smoketests.spec.ts duration=68694
|
||||
// currentRetry=0 speed=undefined err=false
|
||||
console.log(`CypressTestResult ${objToLogAttributes(test)}`);
|
||||
});
|
||||
}
|
||||
|
||||
reportErrors() {
|
||||
this.failures.forEach((failure) => {
|
||||
const suite = failure.suite;
|
||||
const test = failure.title;
|
||||
const error = failure.err;
|
||||
|
||||
// Example
|
||||
// CypressError suite="Smoke tests" test="Login scenario, create test data source, dashboard,
|
||||
// panel, and export scenario" error=false
|
||||
console.error(`CypressError ${objToLogAttributes({ suite, test, error })}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stringify object to be log friendly
|
||||
* @param {Object} obj
|
||||
* @returns {String}
|
||||
*/
|
||||
function objToLogAttributes(obj) {
|
||||
return Object.entries(obj)
|
||||
.map(([key, value]) => `${key}=${formatValue(value)}`)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape double quotes
|
||||
* @param {String} str
|
||||
* @returns
|
||||
*/
|
||||
function escapeQuotes(str) {
|
||||
return String(str).replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the value within double quote if needed
|
||||
* @param {*} value
|
||||
* @returns
|
||||
*/
|
||||
function formatValue(value) {
|
||||
const hasWhiteSpaces = /\s/g.test(value);
|
||||
|
||||
return hasWhiteSpaces ? `"${escapeQuotes(value)}"` : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplify the Mocha test object to get the information we need want to report
|
||||
* @param {Mocha.Test} test
|
||||
* @returns {Object}
|
||||
*/
|
||||
function cleanTest(test) {
|
||||
const err = test.err instanceof Error ? test.err.toString() : false;
|
||||
const testLocation = getTestLocation(test);
|
||||
// Remove the test title
|
||||
const suite = testLocation.slice(0, testLocation.length - 1).join(' > ');
|
||||
|
||||
return {
|
||||
currentRetry: test.currentRetry(),
|
||||
duration: test.duration,
|
||||
speed: test.speed,
|
||||
file: getTestFile(test),
|
||||
suite,
|
||||
title: test.title,
|
||||
err,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the test path in the suite herarchy
|
||||
* @example
|
||||
* ['Root Suite in a file', 'Nested suite', 'test title']
|
||||
*
|
||||
* @param {Mocha.Test} test
|
||||
* @returns {Array<String>}
|
||||
*/
|
||||
function getTestLocation(test) {
|
||||
let path = test.title ? [test.title] : [];
|
||||
|
||||
if (test.parent) {
|
||||
path = getTestLocation(test.parent).concat(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative path to the executed spec file
|
||||
* @param {Mocha.Test} test
|
||||
* @returns {String | null}
|
||||
*/
|
||||
function getTestFile(test) {
|
||||
if (test?.file) {
|
||||
return test?.file;
|
||||
}
|
||||
|
||||
if (test?.parent) {
|
||||
return getTestFile(test.parent);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = LogReporter;
|
||||
@@ -37,6 +37,7 @@ declare -A cypressConfig=(
|
||||
[viewportHeight]=1080
|
||||
[trashAssetsBeforeRuns]=false
|
||||
[videoUploadOnPasses]=false
|
||||
[reporter]=../../e2e/log-reporter.js
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -48,7 +48,7 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-stack/stack v1.8.1
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/gofrs/uuid v4.3.0+incompatible // indirect
|
||||
github.com/gofrs/uuid v4.3.0+incompatible
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/golang/snappy v0.0.4
|
||||
@@ -56,7 +56,6 @@ require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/wire v0.5.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gosimple/slug v1.12.0
|
||||
github.com/grafana/cuetsy v0.1.1
|
||||
github.com/grafana/grafana-aws-sdk v0.11.0
|
||||
github.com/grafana/grafana-azure-sdk-go v1.3.1
|
||||
@@ -71,7 +70,7 @@ require (
|
||||
github.com/jmespath/go-jmespath v0.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/linkedin/goavro/v2 v2.10.0
|
||||
github.com/m3db/prometheus_remote_client_golang v0.4.4
|
||||
github.com/magefile/mage v1.13.0
|
||||
@@ -90,7 +89,7 @@ require (
|
||||
github.com/prometheus/prometheus v1.8.2-0.20211011171444-354d8d2ecfac
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russellhaering/goxmldsig v1.1.1
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf
|
||||
github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f
|
||||
github.com/uber/jaeger-client-go v2.29.1+incompatible
|
||||
@@ -106,19 +105,19 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.0.0
|
||||
go.opentelemetry.io/otel/sdk v1.7.0
|
||||
go.opentelemetry.io/otel/trace v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/crypto v0.3.0
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
|
||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0
|
||||
golang.org/x/net v0.2.0
|
||||
golang.org/x/oauth2 v0.2.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
golang.org/x/tools v0.1.12
|
||||
golang.org/x/time v0.2.0
|
||||
golang.org/x/tools v0.3.0
|
||||
gonum.org/v1/gonum v0.11.0
|
||||
google.golang.org/api v0.84.0
|
||||
google.golang.org/grpc v1.47.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.66.2
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/ldap.v3 v3.1.0
|
||||
gopkg.in/mail.v2 v2.3.1
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
@@ -204,7 +203,7 @@ require (
|
||||
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e // indirect
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -221,16 +220,16 @@ require (
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/weaveworks/promrus v1.2.0 // indirect
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.uber.org/atomic v1.9.0
|
||||
go.uber.org/atomic v1.10.0
|
||||
go.uber.org/goleak v1.1.12 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/sys v0.2.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
|
||||
@@ -255,8 +254,10 @@ require (
|
||||
github.com/google/go-github/v45 v45.2.0
|
||||
github.com/grafana/codejen v0.0.3
|
||||
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f
|
||||
github.com/huandu/xstrings v1.3.1
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/matryer/is v1.4.0
|
||||
github.com/parca-dev/parca v0.12.1
|
||||
github.com/urfave/cli v1.22.9
|
||||
go.etcd.io/etcd/api/v3 v3.5.4
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.32.0
|
||||
@@ -287,10 +288,8 @@ require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa // indirect
|
||||
github.com/googleapis/go-type-adapters v1.0.0 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/memberlist v0.4.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.1 // indirect
|
||||
github.com/invopop/yaml v0.1.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
@@ -300,7 +299,6 @@ require (
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
||||
github.com/parca-dev/parca v0.12.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/segmentio/asm v1.1.4 // indirect
|
||||
@@ -368,25 +366,11 @@ require (
|
||||
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.9-0.20220727151557-61cd9c9353fc
|
||||
|
||||
replace github.com/apache/thrift => github.com/apache/thrift v0.14.1
|
||||
|
||||
replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.10.2
|
||||
|
||||
replace github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.7.7
|
||||
|
||||
replace github.com/tidwall/gjson => github.com/tidwall/gjson v1.14.1
|
||||
|
||||
// Upgraded to fix CVE-2020-26066. This can be removed when go.opentelemetry.io/collector and github.com/influxdata/telegraf are upgraded
|
||||
// github.com/tidwall/match v1.0.1 should not be used.
|
||||
replace github.com/tidwall/match => github.com/tidwall/match v1.1.1
|
||||
|
||||
// Thema's thema CLI requires cobra, which eventually works its way down to go-hclog@v1.0.0.
|
||||
// Upgrading affects backend plugins: https://github.com/grafana/grafana/pull/47653#discussion_r850508593
|
||||
// No harm to Thema because it's only a dependency in its main package.
|
||||
replace github.com/hashicorp/go-hclog => github.com/hashicorp/go-hclog v0.16.1
|
||||
|
||||
replace github.com/microcosm-cc/bluemonday => github.com/microcosm-cc/bluemonday v1.0.18
|
||||
|
||||
// This is a patched v0.8.2 intended to fix session.Find (and others) silently ignoring SQLITE_BUSY errors. This could
|
||||
// happen, for example, during a read when the sqlite db is under heavy write load.
|
||||
// This patch cherry picks compatible fixes from upstream xorm PR#1998 and can be reverted on upgrade to xorm v1.2.0+.
|
||||
|
||||
@@ -144,6 +144,7 @@ github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9B
|
||||
github.com/Azure/go-amqp v0.16.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
|
||||
github.com/Azure/go-amqp v0.16.4/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
@@ -325,7 +326,9 @@ github.com/apache/arrow/go/arrow v0.0.0-20200923215132-ac86123a3f01/go.mod h1:QN
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ=
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ=
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
|
||||
github.com/apache/thrift v0.14.1/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.14.2/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
@@ -373,8 +376,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=
|
||||
@@ -865,6 +869,9 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
@@ -1052,10 +1059,13 @@ github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUri
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.10.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-redis/redis/v8 v8.0.0-beta.10.0.20200905143926-df7fe4e2ce72/go.mod h1:CJP1ZIHwhosNYwIdaHPZK9vHsM3+roNBaZ7U9Of1DXc=
|
||||
@@ -1351,10 +1361,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc=
|
||||
github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
|
||||
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
|
||||
github.com/grafana/cuetsy v0.1.1 h1:+1jaDDYCpvKlcOWJgBRbkc5+VZIClCEn5mbI+4PLZqM=
|
||||
@@ -1730,6 +1736,7 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6Fm
|
||||
github.com/leanovate/gopter v0.2.4/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
|
||||
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+OsPH9rF2u428CIrGL/jLmPsoOQQ4=
|
||||
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leoluk/perflib_exporter v0.1.0/go.mod h1:rpV0lYj7lemdTm31t7zpCqYqPnw7xs86f+BaaNBVYFM=
|
||||
@@ -1746,8 +1753,9 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
|
||||
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.0/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
@@ -1925,6 +1933,7 @@ github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3P
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
|
||||
github.com/mozillazg/go-cos v0.13.0/go.mod h1:Zp6DvvXn0RUOXGJ2chmWt2bLEqRAnJnS3DnAZsJsoaE=
|
||||
@@ -2350,8 +2359,9 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@@ -2363,8 +2373,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
@@ -2383,12 +2394,12 @@ github.com/thanos-io/thanos v0.13.1-0.20210224074000-659446cab117/go.mod h1:kdqF
|
||||
github.com/thanos-io/thanos v0.13.1-0.20210226164558-03dace0a1aa1/go.mod h1:gMCy4oCteKTT7VuXVvXLTPGzzjovX1VPE5p+HgL1hyU=
|
||||
github.com/thanos-io/thanos v0.13.1-0.20210401085038-d7dff0c84d17/go.mod h1:zU8KqE+6A+HksK4wiep8e/3UvCZLm+Wrw9AqZGaAm9k=
|
||||
github.com/thanos-io/thanos v0.22.0/go.mod h1:SZDWz3phcUcBr4MYFoPFRvl+Z9Nbi45HlwQlwSZSt+Q=
|
||||
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0=
|
||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||
github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
|
||||
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/tinylru v1.0.2/go.mod h1:HDVL7TsWeezQ4g44Um84TOVBMFcq7Xa9giqNc805KJ8=
|
||||
github.com/tidwall/wal v0.1.4/go.mod h1:ww7Pd44/KnyETODJPUPKrzLlYjI72GZWlucNKt7pOt0=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
@@ -2622,8 +2633,9 @@ go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/automaxprocs v1.2.0/go.mod h1:YfO3fm683kQpzETxlTGZhGIVmXAhaw3gxeBADbpZtnU=
|
||||
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
|
||||
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
@@ -2708,8 +2720,9 @@ golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -2863,8 +2876,9 @@ 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/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
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=
|
||||
@@ -2892,8 +2906,8 @@ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 h1:VnGaRqoLmqZH/3TMLJwYCEWkR4j1nuIU1U9TvbqsDUw=
|
||||
golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
|
||||
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -3085,14 +3099,17 @@ 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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.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/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
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=
|
||||
@@ -3119,8 +3136,8 @@ golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
|
||||
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -3224,8 +3241,9 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -3355,6 +3373,8 @@ gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBl
|
||||
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
@@ -3363,8 +3383,8 @@ gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ldap.v3 v3.1.0 h1:DIDWEjI7vQWREh0S8X5/NFPCZ3MCVd55LmXKPW4XLGE=
|
||||
gopkg.in/ldap.v3 v3.1.0/go.mod h1:dQjCc0R0kfyFjIlWNMH1DORwUASZyDxo2Ry1B51dXaQ=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
@@ -3486,7 +3506,6 @@ k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C
|
||||
k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
|
||||
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
|
||||
k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE=
|
||||
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM=
|
||||
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
|
||||
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClCayuJCe2/4fZUvF7VG99sU=
|
||||
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "9.2.6",
|
||||
"testing": "9.3.0-beta1"
|
||||
"stable": "9.3.0",
|
||||
"testing": "9.3.0"
|
||||
}
|
||||
|
||||
+1
-1
@@ -348,7 +348,7 @@
|
||||
"prop-types": "15.8.1",
|
||||
"pseudoizer": "^0.1.0",
|
||||
"rc-cascader": "3.7.0",
|
||||
"rc-drawer": "4.4.3",
|
||||
"rc-drawer": "6.0.1",
|
||||
"rc-slider": "10.0.1",
|
||||
"rc-time-picker": "3.7.3",
|
||||
"rc-tree": "5.7.0",
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
"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",
|
||||
|
||||
@@ -291,7 +291,7 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero:
|
||||
} as FieldCalcs;
|
||||
|
||||
const data = field.values;
|
||||
calcs.count = data.length;
|
||||
calcs.count = ignoreNulls ? data.length : data.toArray().filter((val) => val != null).length;
|
||||
|
||||
const isNumberField = field.type === FieldType.number || FieldType.time;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -177,3 +177,6 @@ export interface DataConfigSource {
|
||||
getFieldOverrideOptions: () => ApplyFieldOverrideOptions | undefined;
|
||||
snapshotData?: DataFrameDTO[];
|
||||
}
|
||||
|
||||
type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T;
|
||||
export const isTruthy = <T>(value: T): value is Truthy<T> => Boolean(value);
|
||||
|
||||
@@ -65,9 +65,10 @@ export interface FeatureToggles {
|
||||
internationalization?: boolean;
|
||||
topnav?: boolean;
|
||||
grpcServer?: boolean;
|
||||
objectStore?: boolean;
|
||||
entityStore?: boolean;
|
||||
traceqlEditor?: boolean;
|
||||
flameGraph?: boolean;
|
||||
cloudWatchCrossAccountQuerying?: boolean;
|
||||
redshiftAsyncQueryDataSupport?: boolean;
|
||||
athenaAsyncQueryDataSupport?: boolean;
|
||||
increaseInMemDatabaseQueryCache?: boolean;
|
||||
@@ -81,4 +82,6 @@ export interface FeatureToggles {
|
||||
nestedFolders?: boolean;
|
||||
accessTokenExpirationCheck?: boolean;
|
||||
elasticsearchBackendMigration?: boolean;
|
||||
secureSocksDatasourceProxy?: boolean;
|
||||
authnService?: boolean;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export const availableIconsIndex = {
|
||||
compass: true,
|
||||
copy: true,
|
||||
'credit-card': true,
|
||||
crosshair: true,
|
||||
cube: true,
|
||||
dashboard: true,
|
||||
database: true,
|
||||
@@ -104,6 +105,7 @@ export const availableIconsIndex = {
|
||||
'gf-show-context': true,
|
||||
grafana: true,
|
||||
'graph-bar': true,
|
||||
'grafana-ml': true,
|
||||
heart: true,
|
||||
'heart-rate': true,
|
||||
'heart-break': true,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -107,7 +107,7 @@ export const Components = {
|
||||
expand: 'Drawer expand',
|
||||
contract: 'Drawer contract',
|
||||
close: 'Drawer close',
|
||||
rcContentWrapper: () => '.drawer-content-wrapper',
|
||||
rcContentWrapper: () => '.rc-drawer-content-wrapper',
|
||||
},
|
||||
},
|
||||
PanelEditor: {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const Pages = {
|
||||
url: '/datasources/new',
|
||||
/** @deprecated Use dataSourcePluginsV2 */
|
||||
dataSourcePlugins: (pluginName: string) => `Data source plugin item ${pluginName}`,
|
||||
dataSourcePluginsV2: (pluginName: string) => `Add data source ${pluginName}`,
|
||||
dataSourcePluginsV2: (pluginName: string) => `Add new data source ${pluginName}`,
|
||||
},
|
||||
ConfirmModal: {
|
||||
delete: 'Confirm Modal Danger Button',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -32,7 +32,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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
@@ -79,7 +81,7 @@
|
||||
"ol": "7.1.0",
|
||||
"prismjs": "1.29.0",
|
||||
"rc-cascader": "3.7.0",
|
||||
"rc-drawer": "4.4.3",
|
||||
"rc-drawer": "6.0.1",
|
||||
"rc-slider": "10.0.1",
|
||||
"rc-time-picker": "^3.7.3",
|
||||
"rc-tooltip": "5.2.2",
|
||||
|
||||
@@ -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,7 +3,7 @@ import { useArgs } from '@storybook/client-api';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import { SeriesColorPicker, ColorPicker } from '@grafana/ui';
|
||||
import { SeriesColorPicker, ColorPicker, clearButtonStyles, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
@@ -44,6 +44,7 @@ export const Basic: ComponentStory<typeof ColorPicker> = ({ color, enableNamedCo
|
||||
|
||||
export const SeriesPicker: ComponentStory<typeof SeriesColorPicker> = ({ color, enableNamedColors }) => {
|
||||
const [, updateArgs] = useArgs();
|
||||
const clearButton = useStyles2(clearButtonStyles);
|
||||
return (
|
||||
<SeriesColorPicker
|
||||
enableNamedColors={enableNamedColors}
|
||||
@@ -56,9 +57,16 @@ export const SeriesPicker: ComponentStory<typeof SeriesColorPicker> = ({ color,
|
||||
}}
|
||||
>
|
||||
{({ ref, showColorPicker, hideColorPicker }) => (
|
||||
<div ref={ref} onMouseLeave={hideColorPicker} onClick={showColorPicker} style={{ color, cursor: 'pointer' }}>
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
onMouseLeave={hideColorPicker}
|
||||
onClick={showColorPicker}
|
||||
style={{ color }}
|
||||
className={clearButton}
|
||||
>
|
||||
Open color picker
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</SeriesColorPicker>
|
||||
);
|
||||
|
||||
@@ -69,7 +69,7 @@ export const DataLinkSuggestions: React.FC<DataLinkSuggestionsProps> = ({ sugges
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.wrapper}>
|
||||
<div role="menu" ref={ref} className={styles.wrapper}>
|
||||
{Object.keys(groupedSuggestions).map((key, i) => {
|
||||
const indexOffset =
|
||||
i === 0
|
||||
@@ -116,7 +116,11 @@ const DataLinkSuggestionsList: React.FC<DataLinkSuggestionsListProps> = React.me
|
||||
renderItem={(item, index) => {
|
||||
const isActive = index + activeIndexOffset === activeIndex;
|
||||
return (
|
||||
// key events are handled by DataLinkInput
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
role="menuitem"
|
||||
tabIndex={0}
|
||||
className={cx(styles.item, isActive && styles.activeItem)}
|
||||
ref={isActive ? selectedRef : undefined}
|
||||
onClick={() => {
|
||||
|
||||
@@ -74,24 +74,37 @@ export function Drawer({
|
||||
|
||||
return (
|
||||
<RcDrawer
|
||||
level={null}
|
||||
handler={false}
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
maskClosable={closeOnMaskClick}
|
||||
placement="right"
|
||||
width={currentWidth}
|
||||
getContainer={inline ? undefined : 'body'}
|
||||
style={style}
|
||||
className={drawerStyles.drawer}
|
||||
aria-label={
|
||||
typeof title === 'string'
|
||||
? selectors.components.Drawer.General.title(title)
|
||||
: selectors.components.Drawer.General.title('no title')
|
||||
}
|
||||
className={drawerStyles.drawerContent}
|
||||
rootClassName={drawerStyles.drawer}
|
||||
motion={{
|
||||
motionAppear: true,
|
||||
motionName: drawerStyles.drawerMotion,
|
||||
}}
|
||||
maskClassName={drawerStyles.mask}
|
||||
maskClosable={closeOnMaskClick}
|
||||
maskMotion={{
|
||||
motionAppear: true,
|
||||
motionName: drawerStyles.maskMotion,
|
||||
}}
|
||||
>
|
||||
<FocusScope restoreFocus contain autoFocus>
|
||||
<div className={drawerStyles.container} {...overlayProps} {...dialogProps} ref={overlayRef}>
|
||||
<div
|
||||
aria-label={
|
||||
typeof title === 'string'
|
||||
? selectors.components.Drawer.General.title(title)
|
||||
: selectors.components.Drawer.General.title('no title')
|
||||
}
|
||||
className={drawerStyles.container}
|
||||
{...overlayProps}
|
||||
{...dialogProps}
|
||||
ref={overlayRef}
|
||||
>
|
||||
{typeof title === 'string' && (
|
||||
<div className={drawerStyles.header}>
|
||||
<div className={drawerStyles.actions}>
|
||||
@@ -145,33 +158,46 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
flex: 1 1 0;
|
||||
`,
|
||||
drawer: css`
|
||||
.drawer-content {
|
||||
background-color: ${theme.colors.background.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.drawer-open .drawer-mask {
|
||||
background-color: ${theme.components.overlay.background};
|
||||
backdrop-filter: blur(1px);
|
||||
opacity: 1;
|
||||
}
|
||||
.drawer-mask {
|
||||
background-color: ${theme.components.overlay.background};
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
.drawer-open .drawer-content-wrapper {
|
||||
.rc-drawer-content-wrapper {
|
||||
box-shadow: ${theme.shadows.z3};
|
||||
}
|
||||
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
|
||||
${theme.breakpoints.down('sm')} {
|
||||
.drawer-content-wrapper {
|
||||
${theme.breakpoints.down('sm')} {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
`,
|
||||
drawerContent: css`
|
||||
background-color: ${theme.colors.background.primary} !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
`,
|
||||
drawerMotion: css`
|
||||
&-appear {
|
||||
transform: translateX(100%);
|
||||
transition: none !important;
|
||||
|
||||
&-active {
|
||||
transition: ${theme.transitions.create('transform')} !important;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
`,
|
||||
mask: css`
|
||||
background-color: ${theme.components.overlay.background} !important;
|
||||
backdrop-filter: blur(1px);
|
||||
`,
|
||||
maskMotion: css`
|
||||
&-appear {
|
||||
opacity: 0;
|
||||
|
||||
&-active {
|
||||
opacity: 1;
|
||||
transition: ${theme.transitions.create('opacity')};
|
||||
}
|
||||
}
|
||||
`,
|
||||
header: css`
|
||||
background-color: ${theme.colors.background.canvas};
|
||||
flex-grow: 0;
|
||||
|
||||
@@ -44,6 +44,11 @@ export const Dropdown = React.memo(({ children, overlay, placement }: Props) =>
|
||||
{visible && (
|
||||
<Portal>
|
||||
<FocusScope autoFocus>
|
||||
{/*
|
||||
this is handling bubbled events from the inner overlay
|
||||
see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
|
||||
*/}
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
|
||||
<div ref={setTooltipRef} {...getTooltipProps()} onClick={onOverlayClicked}>
|
||||
<div {...getArrowProps({ className: 'tooltip-arrow' })} />
|
||||
<CSSTransition
|
||||
|
||||
+7
-3
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { CellProps } from 'react-table';
|
||||
|
||||
import { IconButton } from '@grafana/ui';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
const expanderContainerStyles = css`
|
||||
display: flex;
|
||||
@@ -10,14 +10,18 @@ const expanderContainerStyles = css`
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export function ExpanderCell<K extends object>({ row }: CellProps<K, void>) {
|
||||
export function ExpanderCell<K extends object>({ row, __rowID }: CellProps<K, void> & { __rowID: string }) {
|
||||
return (
|
||||
<div className={expanderContainerStyles}>
|
||||
<IconButton
|
||||
tooltip="toggle row expanded"
|
||||
aria-controls={__rowID}
|
||||
// @ts-expect-error react-table doesn't ship with useExpanded types and we can't use declaration merging without affecting the table viz
|
||||
name={row.isExpanded ? 'angle-down' : 'angle-right'}
|
||||
// @ts-expect-error same as the line above
|
||||
{...row.getToggleRowExpandedProps({})}
|
||||
aria-expanded={row.isExpanded}
|
||||
// @ts-expect-error same as the line above
|
||||
{...row.getToggleRowExpandedProps()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,166 @@
|
||||
import { Meta, Props, Story, Canvas } from '@storybook/addon-docs/blocks';
|
||||
|
||||
import { InteractiveTable } from './InteractiveTable';
|
||||
import { Badge } from '../Badge/Badge';
|
||||
|
||||
<Meta title="MDX|InteractiveTable" component={InteractiveTable} />
|
||||
|
||||
# InteractiveTable
|
||||
|
||||
<Badge text="Alpha" icon="rocket" color="blue" tooltip="This component is still experimental." />
|
||||
|
||||
The InteractiveTable is used to display and select data efficiently.
|
||||
It allows for the display and modification of detailed information.
|
||||
With additional functionality it allows for batch editing, as needed by your feature's users.
|
||||
|
||||
It is a wrapper around [React Table](https://react-table-v7.tanstack.com/), for more informations about it, refer to the [official documentation](https://react-table.tanstack.com/docs/overview).
|
||||
|
||||
### When to use
|
||||
|
||||
The InteractiveTable can be used to allow users to perform administrative tasks workflows.
|
||||
|
||||
### When not to use
|
||||
|
||||
Avoid using the InteractiveTable where mobile or responsiveness may be a requirement.
|
||||
Consider an alternative pattern where the user is presented with a summary list and can click/tap to an individual page for each row in that list.
|
||||
|
||||
### Usage
|
||||
|
||||
<Props of={InteractiveTable} />
|
||||
|
||||
#### About `columns` and `data` Props
|
||||
|
||||
To avoid unnecessary rerenders, `columns` and `data` must be memoized.
|
||||
|
||||
Columns are rendered in the same order defined in the `columns` prop.
|
||||
Each Cell's content is automatically rendered by matching the `id` of the column to the key of each object in the `data` array prop.
|
||||
|
||||
##### Example
|
||||
|
||||
```tsx
|
||||
interface TableData {
|
||||
projectName: string;
|
||||
repository: string;
|
||||
}
|
||||
|
||||
const columns = useMemo<Array<Column<TableData>>>(
|
||||
() => [
|
||||
id: 'projectName'
|
||||
header: "Project Name"
|
||||
],
|
||||
[
|
||||
id: 'repository',
|
||||
header: "Repository"
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const data = useMemo<Array<TableData>>(
|
||||
() => [
|
||||
{
|
||||
projectName: 'Grafana',
|
||||
repository: 'https://github.com/grafana/grafana',
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
projectName: 'Loki';
|
||||
repository: 'https://github.com/grafana/loki';
|
||||
}
|
||||
],
|
||||
[]
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### With row expansion
|
||||
|
||||
Individual rows can be expanded to display additional details or reconfigure properties previously defined when the row was created.
|
||||
The expanded row area should be used to declutter the primary presentation of data, carefully consider what the user needs to know at first glance and what can be hidden behind the Row Expander button.
|
||||
|
||||
In general, data-types that are consistent across all dataset are in the primary table, variances are pushed to the expanded section for each individual row.
|
||||
|
||||
<Story id="experimental-interactivetable--with-row-expansion" />
|
||||
|
||||
Row expansion is enabled whenever the `renderExpanded` prop is provided. The `renderExpanded` function is called with the row's data and should return a ReactNode.
|
||||
|
||||
```tsx
|
||||
interface TableData {
|
||||
datasource: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const tableData: TableData[] = [
|
||||
//...
|
||||
];
|
||||
|
||||
const columns: Array<Column<TableData>> = [
|
||||
//...
|
||||
];
|
||||
|
||||
const ExpandedCell = ({ description }: TableData) => {
|
||||
return <p>{description}</p>;
|
||||
};
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
getRowId={(r) => r.datasource}
|
||||
renderExpandedRow={ExpandedCell}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Cell Rendering
|
||||
|
||||
Individual cells can be rendered using custom content dy defining a `cell` property on the column definition.
|
||||
|
||||
<Story id="experimental-interactivetable--with-custom-cell" />
|
||||
|
||||
```tsx
|
||||
interface TableData {
|
||||
datasource: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
const RepoCell = ({
|
||||
row: {
|
||||
original: { repo },
|
||||
},
|
||||
}: CellProps<WithCustomCellData, void>) => {
|
||||
return (
|
||||
<LinkButton href={repo} size="sm" icon="external-link-alt">
|
||||
Open on GitHub
|
||||
</LinkButton>
|
||||
);
|
||||
};
|
||||
|
||||
const tableData: WithCustomCellData[] = [
|
||||
{
|
||||
datasource: 'Prometheus',
|
||||
repo: 'https://github.com/prometheus/prometheus',
|
||||
},
|
||||
{
|
||||
datasource: 'Loki',
|
||||
repo: 'https://github.com/grafana/loki',
|
||||
},
|
||||
{
|
||||
datasource: 'Tempo',
|
||||
repo: 'https://github.com/grafana/tempo',
|
||||
},
|
||||
];
|
||||
|
||||
const columns: Array<Column<WithCustomCellData>> = [
|
||||
{ id: 'datasource', header: 'Data Source' },
|
||||
{ id: 'repo', header: 'Repo', cell: RepoCell },
|
||||
];
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <InteractiveTable columns={columns} data={tableData} getRowId={(r) => r.datasource} />;
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,142 @@
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { InteractiveTable, Column, CellProps, LinkButton } from '@grafana/ui';
|
||||
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
||||
import mdx from './InteractiveTable.mdx';
|
||||
|
||||
const EXCLUDED_PROPS = ['className', 'renderExpandedRow', 'getRowId'];
|
||||
|
||||
const meta: ComponentMeta<typeof InteractiveTable> = {
|
||||
title: 'Experimental/InteractiveTable',
|
||||
component: InteractiveTable,
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
controls: {
|
||||
exclude: EXCLUDED_PROPS,
|
||||
},
|
||||
},
|
||||
args: {},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
interface TableData {
|
||||
header1: string;
|
||||
header2?: number;
|
||||
noheader?: string;
|
||||
}
|
||||
|
||||
export const Basic: ComponentStory<typeof InteractiveTable> = (args) => {
|
||||
const columns = useMemo<Array<Column<TableData>>>(
|
||||
() => [
|
||||
{ id: 'header2', header: 'With missing values', sortType: 'number', disableGrow: true },
|
||||
{
|
||||
id: 'noheader',
|
||||
sortType: 'number',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
const data: TableData[] = useMemo(
|
||||
() => [
|
||||
{ header1: 'a', header2: 1 },
|
||||
{ header1: 'b', noheader: "This column doesn't have an header" },
|
||||
{ header1: 'c', noheader: "But it's still sortable" },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return <InteractiveTable columns={columns} data={data} getRowId={(r) => r.header1} />;
|
||||
};
|
||||
|
||||
interface WithRowExpansionData {
|
||||
datasource: string;
|
||||
repo: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ExpandedCell = ({ description }: WithRowExpansionData) => {
|
||||
return <p>{description}</p>;
|
||||
};
|
||||
|
||||
export const WithRowExpansion: ComponentStory<typeof InteractiveTable> = (args) => {
|
||||
const tableData: WithRowExpansionData[] = [
|
||||
{
|
||||
datasource: 'Prometheus',
|
||||
repo: 'https://github.com/prometheus/prometheus',
|
||||
description: 'Open source time series database & alerting.',
|
||||
},
|
||||
{
|
||||
datasource: 'Loki',
|
||||
repo: 'https://github.com/grafana/loki',
|
||||
description: 'Like Prometheus but for logs. OSS logging solution from Grafana Labs.',
|
||||
},
|
||||
{
|
||||
datasource: 'Tempo',
|
||||
repo: 'https://github.com/grafana/tempo',
|
||||
description: 'High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.',
|
||||
},
|
||||
];
|
||||
|
||||
const columns: Array<Column<WithRowExpansionData>> = [
|
||||
{ id: 'datasource', header: 'Data Source' },
|
||||
{ id: 'repo', header: 'Repo' },
|
||||
];
|
||||
|
||||
return (
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
getRowId={(r) => r.datasource}
|
||||
renderExpandedRow={ExpandedCell}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface WithCustomCellData {
|
||||
datasource: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
const RepoCell = ({
|
||||
row: {
|
||||
original: { repo },
|
||||
},
|
||||
}: CellProps<WithCustomCellData, void>) => {
|
||||
return (
|
||||
<LinkButton href={repo} size="sm" icon="external-link-alt">
|
||||
Open on GithHub
|
||||
</LinkButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithCustomCell: ComponentStory<typeof InteractiveTable> = (args) => {
|
||||
const tableData: WithCustomCellData[] = [
|
||||
{
|
||||
datasource: 'Prometheus',
|
||||
repo: 'https://github.com/prometheus/prometheus',
|
||||
},
|
||||
{
|
||||
datasource: 'Loki',
|
||||
repo: 'https://github.com/grafana/loki',
|
||||
},
|
||||
{
|
||||
datasource: 'Tempo',
|
||||
repo: 'https://github.com/grafana/tempo',
|
||||
},
|
||||
];
|
||||
|
||||
const columns: Array<Column<WithCustomCellData>> = [
|
||||
{ id: 'datasource', header: 'Data Source' },
|
||||
{ id: 'repo', header: 'Repo', cell: RepoCell },
|
||||
];
|
||||
|
||||
return <InteractiveTable columns={columns} data={tableData} getRowId={(r) => r.datasource} />;
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { fireEvent, getByRole, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { InteractiveTable } from './InteractiveTable';
|
||||
import { Column } from './types';
|
||||
|
||||
interface TableData {
|
||||
id: string;
|
||||
country?: string;
|
||||
value?: string;
|
||||
}
|
||||
function getRowId(row: TableData) {
|
||||
return row.id;
|
||||
}
|
||||
|
||||
describe('InteractiveTable', () => {
|
||||
it('should not render hidden columns', () => {
|
||||
const columns: Array<Column<TableData>> = [
|
||||
{ id: 'id', header: 'ID' },
|
||||
{ id: 'country', header: 'Country', visible: () => false },
|
||||
];
|
||||
const data: TableData[] = [
|
||||
{ id: '1', country: 'Sweden' },
|
||||
{ id: '2', country: 'Portugal' },
|
||||
];
|
||||
render(<InteractiveTable columns={columns} data={data} getRowId={getRowId} />);
|
||||
|
||||
expect(screen.getByRole('columnheader', { name: 'ID' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('columnheader', { name: 'Country' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should correctly sort rows', () => {
|
||||
// We are not testing the sorting logic here since it is already tested in react-table,
|
||||
// but instead we are testing that the sorting is applied correctly to the table and correct aria attributes are set
|
||||
// according to https://www.w3.org/WAI/ARIA/apg/example-index/table/sortable-table
|
||||
const columns: Array<Column<TableData>> = [
|
||||
{ id: 'id', header: 'ID' },
|
||||
{ id: 'value', header: 'Value', sortType: 'string' },
|
||||
{ id: 'country', header: 'Country', sortType: 'number' },
|
||||
];
|
||||
const data: TableData[] = [
|
||||
{ id: '1', value: '1', country: 'Sweden' },
|
||||
{ id: '2', value: '3', country: 'Portugal' },
|
||||
{ id: '3', value: '2', country: 'Italy' },
|
||||
];
|
||||
render(<InteractiveTable columns={columns} data={data} getRowId={getRowId} />);
|
||||
|
||||
const valueColumnHeader = screen.getByRole('columnheader', { name: 'Value' });
|
||||
const countryColumnHeader = screen.getByRole('columnheader', { name: 'Country' });
|
||||
const valueColumnSortButton = getByRole(valueColumnHeader, 'button');
|
||||
const countryColumnSortButton = getByRole(countryColumnHeader, 'button');
|
||||
|
||||
expect(valueColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
expect(countryColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
|
||||
fireEvent.click(countryColumnSortButton);
|
||||
expect(valueColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
expect(countryColumnHeader).toHaveAttribute('aria-sort', 'ascending');
|
||||
|
||||
fireEvent.click(valueColumnSortButton);
|
||||
expect(valueColumnHeader).toHaveAttribute('aria-sort', 'ascending');
|
||||
expect(countryColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
|
||||
fireEvent.click(valueColumnSortButton);
|
||||
expect(valueColumnHeader).toHaveAttribute('aria-sort', 'descending');
|
||||
expect(countryColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
|
||||
fireEvent.click(valueColumnSortButton);
|
||||
expect(valueColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
expect(countryColumnHeader).not.toHaveAttribute('aria-sort');
|
||||
});
|
||||
|
||||
it('correctly expands rows', () => {
|
||||
const columns: Array<Column<TableData>> = [{ id: 'id', header: 'ID' }];
|
||||
const data: TableData[] = [{ id: '1', value: '1', country: 'Sweden' }];
|
||||
render(
|
||||
<InteractiveTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
renderExpandedRow={(row) => <div data-testid={`test-${row.id}`}>{row.country}</div>}
|
||||
/>
|
||||
);
|
||||
|
||||
const expanderButton = screen.getByRole('button', { name: /toggle row expanded/i });
|
||||
fireEvent.click(expanderButton);
|
||||
|
||||
expect(screen.getByTestId('test-1')).toHaveTextContent('Sweden');
|
||||
|
||||
expect(expanderButton.getAttribute('aria-controls')).toBe(
|
||||
// anchestor tr's id should match the expander button's aria-controls attribute
|
||||
screen.getByTestId('test-1').parentElement?.parentElement?.id
|
||||
);
|
||||
});
|
||||
});
|
||||
+123
-47
@@ -1,20 +1,14 @@
|
||||
import { cx, css } from '@emotion/css';
|
||||
import React, { useMemo, Fragment, ReactNode } from 'react';
|
||||
import {
|
||||
CellProps,
|
||||
SortByFn,
|
||||
useExpanded,
|
||||
useSortBy,
|
||||
useTable,
|
||||
DefaultSortTypes,
|
||||
TableOptions,
|
||||
IdType,
|
||||
} from 'react-table';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { useMemo, Fragment, ReactNode, useCallback } from 'react';
|
||||
import { useExpanded, useSortBy, useTable, TableOptions, Row, HeaderGroup } from 'react-table';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, useStyles2 } from '@grafana/ui';
|
||||
import { isTruthy } from 'app/core/utils/types';
|
||||
import { GrafanaTheme2, isTruthy } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { Column } from './types';
|
||||
import { EXPANDER_CELL_ID, getColumns } from './utils';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
@@ -23,47 +17,74 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
border: solid 1px ${theme.colors.border.weak};
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
width: 100%;
|
||||
|
||||
td {
|
||||
padding: ${theme.spacing(1)};
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: ${theme.spacing(1)};
|
||||
min-width: ${theme.spacing(3)};
|
||||
}
|
||||
`,
|
||||
evenRow: css`
|
||||
background: ${theme.colors.background.primary};
|
||||
`,
|
||||
shrink: css`
|
||||
disableGrow: css`
|
||||
width: 0%;
|
||||
`,
|
||||
header: css`
|
||||
&,
|
||||
& > button {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
padding: ${theme.spacing(1)};
|
||||
}
|
||||
& > button {
|
||||
&:after {
|
||||
content: '\\00a0';
|
||||
}
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding-right: ${theme.spacing(2.5)};
|
||||
text-align: left;
|
||||
&:hover {
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.secondary, 0.05)};
|
||||
}
|
||||
}
|
||||
`,
|
||||
sortableHeader: css`
|
||||
/* increases selector's specificity so that it always takes precedence over default styles */
|
||||
&& {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
export interface Column<TableData extends object> {
|
||||
/**
|
||||
* ID of the column.
|
||||
* Set this to the matching object key of your data or `undefined` if the column doesn't have any associated data with it.
|
||||
* This must be unique among all other columns.
|
||||
*/
|
||||
id?: IdType<TableData>;
|
||||
cell?: (props: CellProps<TableData>) => ReactNode;
|
||||
header?: (() => ReactNode | string) | string;
|
||||
sortType?: DefaultSortTypes | SortByFn<TableData>;
|
||||
shrink?: boolean;
|
||||
visible?: (col: TableData[]) => boolean;
|
||||
}
|
||||
|
||||
interface Props<TableData extends object> {
|
||||
/**
|
||||
* Table's columns definition. Must be memoized.
|
||||
*/
|
||||
columns: Array<Column<TableData>>;
|
||||
/**
|
||||
* The data to display in the table. Must be memoized.
|
||||
*/
|
||||
data: TableData[];
|
||||
renderExpandedRow?: (row: TableData) => JSX.Element;
|
||||
/**
|
||||
* Render function for the expanded row. if not provided, the tables rows will not be expandable.
|
||||
*/
|
||||
renderExpandedRow?: (row: TableData) => ReactNode;
|
||||
className?: string;
|
||||
/**
|
||||
* Must return a unique id for each row
|
||||
*/
|
||||
getRowId: TableOptions<TableData>['getRowId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* non-viz table component.
|
||||
* Will need most likely to be moved in @grafana/ui
|
||||
*/
|
||||
export function Table<TableData extends object>({
|
||||
/** @alpha */
|
||||
export function InteractiveTable<TableData extends object>({
|
||||
data,
|
||||
className,
|
||||
columns,
|
||||
@@ -75,6 +96,13 @@ export function Table<TableData extends object>({
|
||||
const cols = getColumns<TableData>(columns);
|
||||
return cols;
|
||||
}, [columns]);
|
||||
const id = useUniqueId();
|
||||
const getRowHTMLID = useCallback(
|
||||
(row: Row<TableData>) => {
|
||||
return `${id}-${row.id}`.replace(/\s/g, '');
|
||||
},
|
||||
[id]
|
||||
);
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable<TableData>(
|
||||
{
|
||||
@@ -82,12 +110,13 @@ export function Table<TableData extends object>({
|
||||
data,
|
||||
autoResetExpanded: false,
|
||||
autoResetSortBy: false,
|
||||
disableMultiSort: true,
|
||||
getRowId,
|
||||
initialState: {
|
||||
hiddenColumns: [
|
||||
!renderExpandedRow && EXPANDER_CELL_ID,
|
||||
...tableColumns
|
||||
.filter((col) => !(col.visible?.(data) ?? true))
|
||||
.filter((col) => !(col.visible ? col.visible(data) : true))
|
||||
.map((c) => c.id)
|
||||
.filter(isTruthy),
|
||||
].filter(isTruthy),
|
||||
@@ -96,6 +125,7 @@ export function Table<TableData extends object>({
|
||||
useSortBy,
|
||||
useExpanded
|
||||
);
|
||||
|
||||
// This should be called only for rows thar we'd want to actually render, which is all at this stage.
|
||||
// We may want to revisit this if we decide to add pagination and/or virtualized tables.
|
||||
rows.forEach(prepareRow);
|
||||
@@ -109,16 +139,19 @@ export function Table<TableData extends object>({
|
||||
return (
|
||||
<tr key={key} {...headerRowProps}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
// TODO: if the column is a function, it should also provide an accessible name as a string to be used a the column title in getSortByToggleProps
|
||||
const { key, ...headerCellProps } = column.getHeaderProps(
|
||||
column.canSort ? column.getSortByToggleProps() : undefined
|
||||
);
|
||||
const { key, ...headerCellProps } = column.getHeaderProps();
|
||||
|
||||
return (
|
||||
<th key={key} className={cx(column.width === 0 && styles.shrink)} {...headerCellProps}>
|
||||
{column.render('Header')}
|
||||
|
||||
{column.isSorted && <Icon name={column.isSortedDesc ? 'angle-down' : 'angle-up'} />}
|
||||
<th
|
||||
key={key}
|
||||
className={cx(styles.header, {
|
||||
[styles.disableGrow]: column.width === 0,
|
||||
[styles.sortableHeader]: column.canSort,
|
||||
})}
|
||||
{...headerCellProps}
|
||||
{...(column.isSorted && { 'aria-sort': column.isSortedDesc ? 'descending' : 'ascending' })}
|
||||
>
|
||||
<ColumnHeader column={column} />
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
@@ -131,6 +164,7 @@ export function Table<TableData extends object>({
|
||||
{rows.map((row, rowIndex) => {
|
||||
const className = cx(rowIndex % 2 === 0 && styles.evenRow);
|
||||
const { key, ...otherRowProps } = row.getRowProps();
|
||||
const rowId = getRowHTMLID(row);
|
||||
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
@@ -139,7 +173,7 @@ export function Table<TableData extends object>({
|
||||
const { key, ...otherCellProps } = cell.getCellProps();
|
||||
return (
|
||||
<td key={key} {...otherCellProps}>
|
||||
{cell.render('Cell')}
|
||||
{cell.render('Cell', { __rowID: rowId })}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
@@ -147,7 +181,7 @@ export function Table<TableData extends object>({
|
||||
{
|
||||
// @ts-expect-error react-table doesn't ship with useExpanded types and we can't use declaration merging without affecting the table viz
|
||||
row.isExpanded && renderExpandedRow && (
|
||||
<tr className={className} {...otherRowProps}>
|
||||
<tr className={className} {...otherRowProps} id={rowId}>
|
||||
<td colSpan={row.cells.length}>{renderExpandedRow(row.original)}</td>
|
||||
</tr>
|
||||
)
|
||||
@@ -159,3 +193,45 @@ export function Table<TableData extends object>({
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
const useUniqueId = () => {
|
||||
return useMemo(() => uniqueId('InteractiveTable'), []);
|
||||
};
|
||||
|
||||
const getColumnheaderStyles = (theme: GrafanaTheme2) => ({
|
||||
sortIcon: css`
|
||||
position: absolute;
|
||||
top: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
|
||||
function ColumnHeader<T extends object>({
|
||||
column: { canSort, render, isSorted, isSortedDesc, getSortByToggleProps },
|
||||
}: {
|
||||
column: HeaderGroup<T>;
|
||||
}) {
|
||||
const styles = useStyles2(getColumnheaderStyles);
|
||||
const { onClick } = getSortByToggleProps();
|
||||
|
||||
const children = (
|
||||
<>
|
||||
{render('Header')}
|
||||
|
||||
{isSorted && (
|
||||
<span aria-hidden="true" className={styles.sortIcon}>
|
||||
<Icon name={isSortedDesc ? 'angle-down' : 'angle-up'} />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (canSort) {
|
||||
return (
|
||||
<button type="button" onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { CellProps, DefaultSortTypes, IdType, SortByFn } from 'react-table';
|
||||
|
||||
export interface Column<TableData extends object> {
|
||||
/**
|
||||
* ID of the column. Must be unique among all other columns
|
||||
*/
|
||||
id: IdType<TableData>;
|
||||
/**
|
||||
* Custom render function for te cell
|
||||
*/
|
||||
cell?: (props: CellProps<TableData>) => ReactNode;
|
||||
/**
|
||||
* Header name. if `undefined` the header will be empty. Useful for action columns.
|
||||
*/
|
||||
header?: string;
|
||||
/**
|
||||
* Column sort type. If `undefined` the column will not be sortable.
|
||||
* */
|
||||
sortType?: DefaultSortTypes | SortByFn<TableData>;
|
||||
/**
|
||||
* If `true` prevents the column from growing more than its content.
|
||||
*/
|
||||
disableGrow?: boolean;
|
||||
/**
|
||||
* If the provided function returns `false` the column will be hidden.
|
||||
*/
|
||||
visible?: (data: TableData[]) => boolean;
|
||||
}
|
||||
+5
-6
@@ -1,11 +1,9 @@
|
||||
import { uniqueId } from 'lodash';
|
||||
import { Column as RTColumn } from 'react-table';
|
||||
|
||||
import { ExpanderCell } from './ExpanderCell';
|
||||
import { Column } from './types';
|
||||
|
||||
import { Column } from '.';
|
||||
|
||||
export const EXPANDER_CELL_ID = '__expander';
|
||||
export const EXPANDER_CELL_ID = '__expander' as const;
|
||||
|
||||
type InternalColumn<T extends object> = RTColumn<T> & {
|
||||
visible?: (data: T[]) => boolean;
|
||||
@@ -24,11 +22,12 @@ export function getColumns<K extends object>(columns: Array<Column<K>>): Array<I
|
||||
// column.id being possibly undefined and not keyof T (where T is the data object)
|
||||
// We do not want to be that strict as we simply pass undefined to cells that do not have data associated with them.
|
||||
...columns.map((column) => ({
|
||||
id: column.id,
|
||||
accessor: column.id,
|
||||
Header: column.header || (() => null),
|
||||
accessor: column.id || uniqueId(),
|
||||
sortType: column.sortType || 'alphanumeric',
|
||||
disableSortBy: !Boolean(column.sortType),
|
||||
width: column.shrink ? 0 : undefined,
|
||||
width: column.disableGrow ? 0 : undefined,
|
||||
visible: column.visible,
|
||||
...(column.cell && { Cell: column.cell }),
|
||||
})),
|
||||
@@ -40,9 +40,7 @@ export const FooterRow = (props: FooterRowProps) => {
|
||||
data-testid={e2eSelectorsTable.footer}
|
||||
style={height ? { height: `${height}px` } : undefined}
|
||||
>
|
||||
{footerGroup.headers.map((column: ColumnInstance, index: number) =>
|
||||
renderFooterCell(column, tableStyles, height)
|
||||
)}
|
||||
{footerGroup.headers.map((column: ColumnInstance) => renderFooterCell(column, tableStyles, height))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -71,10 +69,19 @@ function renderFooterCell(column: ColumnInstance, tableStyles: TableStyles, heig
|
||||
);
|
||||
}
|
||||
|
||||
export function getFooterValue(index: number, footerValues?: FooterItem[]) {
|
||||
export function getFooterValue(index: number, footerValues?: FooterItem[], isCountRowsSet?: boolean) {
|
||||
if (footerValues === undefined) {
|
||||
return EmptyCell;
|
||||
}
|
||||
|
||||
if (isCountRowsSet) {
|
||||
const count = footerValues[index];
|
||||
if (typeof count !== 'string') {
|
||||
return EmptyCell;
|
||||
}
|
||||
|
||||
return FooterCell({ value: [{ Count: count }] });
|
||||
}
|
||||
|
||||
return FooterCell({ value: footerValues[index] });
|
||||
}
|
||||
|
||||
@@ -399,6 +399,121 @@ describe('Table', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('on table footer enabled and count calculation selected', () => {
|
||||
it('should show count of non-null values', async () => {
|
||||
getTestContext({
|
||||
footerOptions: { show: true, reducer: ['count'] },
|
||||
data: toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'number',
|
||||
type: FieldType.number,
|
||||
values: [1, 1, 1, 2, null],
|
||||
config: {
|
||||
custom: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('4');
|
||||
});
|
||||
|
||||
it('should show count of rows when `count rows` is selected', async () => {
|
||||
getTestContext({
|
||||
footerOptions: { show: true, reducer: ['count'], countRows: true },
|
||||
data: toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'number1',
|
||||
type: FieldType.number,
|
||||
values: [1, 1, 1, 2, null],
|
||||
config: {
|
||||
custom: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual(
|
||||
'Count:'
|
||||
);
|
||||
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
|
||||
});
|
||||
|
||||
it('should show correct counts when turning `count rows` on and off', async () => {
|
||||
const { rerender } = getTestContext({
|
||||
footerOptions: { show: true, reducer: ['count'], countRows: true },
|
||||
data: toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'number1',
|
||||
type: FieldType.number,
|
||||
values: [1, 1, 1, 2, null],
|
||||
config: {
|
||||
custom: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual(
|
||||
'Count:'
|
||||
);
|
||||
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
|
||||
|
||||
const onSortByChange = jest.fn();
|
||||
const onCellFilterAdded = jest.fn();
|
||||
const onColumnResize = jest.fn();
|
||||
const props: Props = {
|
||||
ariaLabel: 'aria-label',
|
||||
data: getDefaultDataFrame(),
|
||||
height: 600,
|
||||
width: 800,
|
||||
onSortByChange,
|
||||
onCellFilterAdded,
|
||||
onColumnResize,
|
||||
};
|
||||
|
||||
const propOverrides = {
|
||||
footerOptions: { show: true, reducer: ['count'], countRows: false },
|
||||
data: toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'number',
|
||||
type: FieldType.number,
|
||||
values: [1, 1, 1, 2, null],
|
||||
config: {
|
||||
custom: {
|
||||
filterable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
rerender(<Table {...props} />);
|
||||
|
||||
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('4');
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
|
||||
import { DataFrame, getFieldDisplayName, Field } from '@grafana/data';
|
||||
import { DataFrame, getFieldDisplayName, Field, ReducerID } from '@grafana/data';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||
@@ -188,10 +188,27 @@ export const Table = memo((props: Props) => {
|
||||
return Array(data.length).fill(0);
|
||||
}, [data]);
|
||||
|
||||
const isCountRowsSet = Boolean(
|
||||
footerOptions?.countRows &&
|
||||
footerOptions.reducer &&
|
||||
footerOptions.reducer.length &&
|
||||
footerOptions.reducer[0] === ReducerID.count
|
||||
);
|
||||
|
||||
// React-table column definitions
|
||||
const memoizedColumns = useMemo(
|
||||
() => getColumns(data, width, columnMinWidth, expandedIndexes, setExpandedIndexes, !!subData?.length, footerItems),
|
||||
[data, width, columnMinWidth, footerItems, subData, expandedIndexes]
|
||||
() =>
|
||||
getColumns(
|
||||
data,
|
||||
width,
|
||||
columnMinWidth,
|
||||
expandedIndexes,
|
||||
setExpandedIndexes,
|
||||
!!subData?.length,
|
||||
footerItems,
|
||||
isCountRowsSet
|
||||
),
|
||||
[data, width, columnMinWidth, footerItems, subData, expandedIndexes, isCountRowsSet]
|
||||
);
|
||||
|
||||
// Internal react table state reducer
|
||||
@@ -244,17 +261,24 @@ export const Table = memo((props: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (footerOptions.show) {
|
||||
setFooterItems(
|
||||
getFooterItems(
|
||||
headerGroups[0].headers as unknown as Array<{ field: Field }>,
|
||||
createFooterCalculationValues(rows),
|
||||
footerOptions,
|
||||
theme
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if (!footerOptions.show) {
|
||||
setFooterItems(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const footerItems = getFooterItems(
|
||||
headerGroups[0].headers as unknown as Array<{ field: Field }>,
|
||||
createFooterCalculationValues(rows),
|
||||
footerOptions,
|
||||
theme
|
||||
);
|
||||
|
||||
if (isCountRowsSet) {
|
||||
const footerItemsCountRows: FooterItem[] = new Array(footerItems.length).fill(undefined);
|
||||
footerItemsCountRows[0] = data.length.toString();
|
||||
setFooterItems(footerItemsCountRows);
|
||||
} else {
|
||||
setFooterItems(footerItems);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [footerOptions, theme, state.filters, data]);
|
||||
@@ -305,6 +329,10 @@ export const Table = memo((props: Props) => {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedIndexes(new Set());
|
||||
}, [data, subData]);
|
||||
|
||||
const renderSubTable = React.useCallback(
|
||||
(rowIndex: number) => {
|
||||
if (expandedIndexes.has(rowIndex)) {
|
||||
|
||||
@@ -50,4 +50,5 @@ export interface TableFooterCalc {
|
||||
reducer: string[]; // actually 1 value
|
||||
fields?: string[];
|
||||
enablePagination?: boolean;
|
||||
countRows?: boolean;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ export function getColumns(
|
||||
expandedIndexes: Set<number>,
|
||||
setExpandedIndexes: (indexes: Set<number>) => void,
|
||||
expander: boolean,
|
||||
footerValues?: FooterItem[]
|
||||
footerValues?: FooterItem[],
|
||||
isCountRowsSet?: boolean
|
||||
): GrafanaTableColumn[] {
|
||||
const columns: GrafanaTableColumn[] = expander
|
||||
? [
|
||||
@@ -134,7 +135,7 @@ export function getColumns(
|
||||
minWidth: fieldTableOptions.minWidth ?? columnMinWidth,
|
||||
filter: memoizeOne(filterByValue(field)),
|
||||
justifyContent: getTextAlign(field),
|
||||
Footer: getFooterValue(fieldIndex, footerValues),
|
||||
Footer: getFooterValue(fieldIndex, footerValues, isCountRowsSet),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ export {
|
||||
} from './DateTimePickers/DatePickerWithInput/DatePickerWithInput';
|
||||
export { DateTimePicker } from './DateTimePickers/DateTimePicker/DateTimePicker';
|
||||
export { List } from './List/List';
|
||||
export { InteractiveTable } from './InteractiveTable/InteractiveTable';
|
||||
export { TagsInput } from './TagsInput/TagsInput';
|
||||
export { Pagination } from './Pagination/Pagination';
|
||||
export { Tag, type OnTagClick } from './Tags/Tag';
|
||||
|
||||
@@ -80,7 +80,7 @@ export const PlotLegend: React.FC<PlotLegendProps> = React.memo(
|
||||
...field,
|
||||
config: {
|
||||
...field.config,
|
||||
unit: 'percent',
|
||||
unit: 'percentunit',
|
||||
},
|
||||
},
|
||||
theme,
|
||||
|
||||
@@ -90,7 +90,8 @@ export function getPageStyles(theme: GrafanaTheme2) {
|
||||
align-items: flex-start;
|
||||
|
||||
> a,
|
||||
> button {
|
||||
> button,
|
||||
> div:nth-child(2) {
|
||||
margin-left: ${theme.spacing(2)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './forms';
|
||||
export * from './icon';
|
||||
export * from './select';
|
||||
export * from './size';
|
||||
export * from './interactiveTable';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { Column } from '../components/InteractiveTable/types';
|
||||
export type { CellProps, SortByFn } from 'react-table';
|
||||
+37
-45
@@ -19,20 +19,22 @@ import traceGenerator from '../demo/trace-generators';
|
||||
import { getTraceName } from '../model/trace-viewer';
|
||||
import transformTraceData from '../model/transform-trace-data';
|
||||
|
||||
import TracePageHeader from './TracePageHeader';
|
||||
import TracePageHeader, { TracePageHeaderEmbedProps } from './TracePageHeader';
|
||||
|
||||
const trace = transformTraceData(traceGenerator.trace({}));
|
||||
const setup = (propOverrides) => {
|
||||
const setup = (propOverrides?: TracePageHeaderEmbedProps) => {
|
||||
const defaultProps = {
|
||||
canCollapse: false,
|
||||
hideSummary: false,
|
||||
onSlimViewClicked: () => {},
|
||||
onTraceGraphViewClicked: () => {},
|
||||
slimView: false,
|
||||
trace,
|
||||
hideMap: false,
|
||||
showArchiveButton: false,
|
||||
showShortcutsHelp: false,
|
||||
showStandaloneLink: false,
|
||||
showViewOptions: false,
|
||||
textFilter: '',
|
||||
viewRange: { time: { current: [10, 20] } },
|
||||
updateTextFilter: () => {},
|
||||
timeZone: '',
|
||||
viewRange: { time: { current: [10, 20] as [number, number] } },
|
||||
updateNextViewRangeTime: () => {},
|
||||
updateViewRangeTime: () => {},
|
||||
...propOverrides,
|
||||
};
|
||||
|
||||
@@ -42,23 +44,23 @@ const setup = (propOverrides) => {
|
||||
describe('TracePageHeader test', () => {
|
||||
it('should render a header ', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByRole('banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render nothing if a trace is not present', () => {
|
||||
setup({ trace: null });
|
||||
|
||||
setup({ trace: null } as TracePageHeaderEmbedProps);
|
||||
expect(screen.queryByRole('banner')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('heading', { traceName: getTraceName(trace.spans) })).not.toBeInTheDocument();
|
||||
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
|
||||
expect(screen.queryByText(/Reset Selection/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the trace title', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByRole('heading', { traceName: getTraceName(trace.spans) })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
name: (content) => content.replace(/ /g, '').startsWith(getTraceName(trace!.spans).replace(/ /g, '')),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the header items', () => {
|
||||
@@ -68,61 +70,51 @@ describe('TracePageHeader test', () => {
|
||||
|
||||
expect(headerItems).toHaveLength(5);
|
||||
// Year-month-day hour-minute-second
|
||||
expect(headerItems[0].textContent.match(/Trace Start:\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3}/g)).toBeTruthy();
|
||||
expect(headerItems[1].textContent.match(/Duration:[\d|\.][\.|\d|s][\.|\d|s]?[\d]?/)).toBeTruthy();
|
||||
expect(headerItems[2].textContent.match(/Services:\d\d?/g)).toBeTruthy();
|
||||
expect(headerItems[3].textContent.match(/Depth:\d\d?/)).toBeTruthy();
|
||||
expect(headerItems[4].textContent.match(/Total Spans:\d\d?\d?\d?/)).toBeTruthy();
|
||||
expect(headerItems[0].textContent?.match(/Trace Start:\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3}/g)).toBeTruthy();
|
||||
expect(headerItems[1].textContent?.match(/Duration:[\d|\.][\.|\d|s][\.|\d|s]?[\d]?/)).toBeTruthy();
|
||||
expect(headerItems[2].textContent?.match(/Services:\d\d?/g)).toBeTruthy();
|
||||
expect(headerItems[3].textContent?.match(/Depth:\d\d?/)).toBeTruthy();
|
||||
expect(headerItems[4].textContent?.match(/Total Spans:\d\d?\d?\d?/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render a <SpanGraph>', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByText(/Reset Selection/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('observes the visibility toggles for various UX elements', () => {
|
||||
it('hides the minimap when hideMap === true', () => {
|
||||
setup({ hideMap: true });
|
||||
|
||||
setup({ hideMap: true } as TracePageHeaderEmbedProps);
|
||||
expect(screen.queryByText(/Reset Selection/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the summary when hideSummary === true', () => {
|
||||
const { rerender } = setup({ hideSummary: false });
|
||||
const { rerender } = setup({ hideSummary: false } as TracePageHeaderEmbedProps);
|
||||
expect(screen.queryAllByRole('listitem')).toHaveLength(5);
|
||||
|
||||
rerender(<TracePageHeader hideSummary={false} trace={null} />);
|
||||
rerender(<TracePageHeader {...({ hideSummary: false, trace: null } as TracePageHeaderEmbedProps)} />);
|
||||
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
|
||||
|
||||
rerender(
|
||||
<TracePageHeader
|
||||
trace={trace}
|
||||
hideSummary={true}
|
||||
hideMap={false}
|
||||
showArchiveButton={false}
|
||||
showShortcutsHelp={false}
|
||||
showStandaloneLink={false}
|
||||
showViewOptions={false}
|
||||
textFilter=""
|
||||
viewRange={{ time: { current: [10, 20] } }}
|
||||
updateTextFilter={() => {}}
|
||||
{...({
|
||||
trace: trace,
|
||||
hideSummary: true,
|
||||
hideMap: false,
|
||||
viewRange: { time: { current: [10, 20] } },
|
||||
} as TracePageHeaderEmbedProps)}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
|
||||
|
||||
rerender(
|
||||
<TracePageHeader
|
||||
trace={trace}
|
||||
hideSummary={false}
|
||||
hideMap={false}
|
||||
showArchiveButton={false}
|
||||
showShortcutsHelp={false}
|
||||
showStandaloneLink={false}
|
||||
showViewOptions={false}
|
||||
textFilter=""
|
||||
viewRange={{ time: { current: [10, 20] } }}
|
||||
updateTextFilter={() => {}}
|
||||
{...({
|
||||
trace: trace,
|
||||
hideSummary: false,
|
||||
hideMap: false,
|
||||
viewRange: { time: { current: [10, 20] } },
|
||||
} as TracePageHeaderEmbedProps)}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryAllByRole('listitem')).toHaveLength(5);
|
||||
@@ -140,14 +140,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
};
|
||||
};
|
||||
|
||||
type TracePageHeaderEmbedProps = {
|
||||
export type TracePageHeaderEmbedProps = {
|
||||
canCollapse: boolean;
|
||||
hideMap: boolean;
|
||||
hideSummary: boolean;
|
||||
onSlimViewClicked: () => void;
|
||||
onTraceGraphViewClicked: () => void;
|
||||
slimView: boolean;
|
||||
trace: Trace;
|
||||
trace: Trace | null;
|
||||
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
|
||||
updateViewRangeTime: TUpdateViewRangeTimeFunction;
|
||||
viewRange: ViewRange;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2019 Uber Technologies, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import UiFindInput from './UiFindInput';
|
||||
|
||||
describe('UiFindInput', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders as expected with no value', () => {
|
||||
render(<UiFindInput value={undefined} />);
|
||||
const uiFindInput = screen.queryByPlaceholderText('Find...');
|
||||
expect(uiFindInput).toBeInTheDocument();
|
||||
expect(uiFindInput['value']).toEqual('');
|
||||
});
|
||||
|
||||
it('renders as expected with value', () => {
|
||||
render(<UiFindInput value="value" />);
|
||||
const uiFindInput = screen.queryByPlaceholderText('Find...');
|
||||
expect(uiFindInput).toBeInTheDocument();
|
||||
expect(uiFindInput['value']).toEqual('value');
|
||||
});
|
||||
});
|
||||
});
|
||||
+23
-17
@@ -12,11 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { TraceResponse } from 'src/types';
|
||||
import { TraceSpan, TraceSpanData } from 'src/types/trace';
|
||||
|
||||
import traceGenerator from '../demo/trace-generators';
|
||||
|
||||
import * as spanSelectors from './span';
|
||||
|
||||
const generatedTrace = traceGenerator.trace({ numberOfSpans: 45 });
|
||||
const generatedTrace: TraceResponse = traceGenerator.trace({ numberOfSpans: 45 });
|
||||
|
||||
it('getSpanId() should return the name of the span', () => {
|
||||
const span = generatedTrace.spans[0];
|
||||
@@ -46,8 +49,10 @@ it('getSpanReferences() should return the span reference array', () => {
|
||||
expect(spanSelectors.getSpanReferences(generatedTrace.spans[0])).toEqual(generatedTrace.spans[0].references);
|
||||
});
|
||||
|
||||
it('getSpanReferences() should return empty array for null references', () => {
|
||||
expect(spanSelectors.getSpanReferences({ references: null })).toEqual([]);
|
||||
it('getSpanReferences() should return an empty array when references is undefined', () => {
|
||||
const span = generatedTrace.spans[0];
|
||||
span.references = undefined;
|
||||
expect(spanSelectors.getSpanReferences(span)).toEqual([]);
|
||||
});
|
||||
|
||||
it('getSpanReferenceByType() should return the span reference requested', () => {
|
||||
@@ -55,7 +60,7 @@ it('getSpanReferenceByType() should return the span reference requested', () =>
|
||||
spanSelectors.getSpanReferenceByType({
|
||||
span: generatedTrace.spans[1],
|
||||
type: 'CHILD_OF',
|
||||
}).refType
|
||||
})?.refType
|
||||
).toBe('CHILD_OF');
|
||||
});
|
||||
|
||||
@@ -70,7 +75,7 @@ it('getSpanReferenceByType() should return undefined if one does not exist', ()
|
||||
|
||||
it('getSpanParentId() should return the spanID of the parent span', () => {
|
||||
expect(spanSelectors.getSpanParentId(generatedTrace.spans[1])).toBe(
|
||||
generatedTrace.spans[1].references.find(({ refType }) => refType === 'CHILD_OF').spanID
|
||||
generatedTrace.spans[1].references!.find(({ refType }: { refType: string }) => refType === 'CHILD_OF')!.spanID
|
||||
);
|
||||
});
|
||||
|
||||
@@ -85,16 +90,17 @@ it('getSpanProcessId() should return the processID of the span', () => {
|
||||
});
|
||||
|
||||
it('getSpanProcess() should return the process of the span', () => {
|
||||
const serviceName = 'bagel';
|
||||
const span = {
|
||||
...generatedTrace.spans[0],
|
||||
process: {},
|
||||
};
|
||||
process: { serviceName },
|
||||
} as TraceSpan;
|
||||
|
||||
expect(spanSelectors.getSpanProcess(span)).toBe(span.process);
|
||||
});
|
||||
|
||||
it('getSpanProcess() should throw if no process exists', () => {
|
||||
expect(() => spanSelectors.getSpanProcess(generatedTrace.spans[0])).toThrow();
|
||||
expect(() => spanSelectors.getSpanProcess(generatedTrace.spans[0] as TraceSpan)).toThrow();
|
||||
});
|
||||
|
||||
it('getSpanServiceName() should return the service name of the span', () => {
|
||||
@@ -102,7 +108,7 @@ it('getSpanServiceName() should return the service name of the span', () => {
|
||||
const span = {
|
||||
...generatedTrace.spans[0],
|
||||
process: { serviceName },
|
||||
};
|
||||
} as TraceSpan;
|
||||
|
||||
expect(spanSelectors.getSpanServiceName(span)).toBe(serviceName);
|
||||
});
|
||||
@@ -112,17 +118,17 @@ it('filterSpansForTimestamps() should return a filtered list of spans between th
|
||||
const spans = [
|
||||
{
|
||||
startTime: now - 1000,
|
||||
id: 'start-time-1',
|
||||
spanID: 'start-time-1',
|
||||
},
|
||||
{
|
||||
startTime: now,
|
||||
id: 'start-time-2',
|
||||
spanID: 'start-time-2',
|
||||
},
|
||||
{
|
||||
startTime: now + 1000,
|
||||
id: 'start-time-3',
|
||||
spanID: 'start-time-3',
|
||||
},
|
||||
];
|
||||
] as TraceSpanData[];
|
||||
|
||||
expect(
|
||||
spanSelectors.filterSpansForTimestamps({
|
||||
@@ -164,23 +170,23 @@ it('filterSpansForText() should return a filtered list of spans between the time
|
||||
process: {
|
||||
serviceName: 'alpha',
|
||||
},
|
||||
id: 'start-time-1',
|
||||
spanID: 'start-time-1',
|
||||
},
|
||||
{
|
||||
operationName: 'GET /another',
|
||||
process: {
|
||||
serviceName: 'beta',
|
||||
},
|
||||
id: 'start-time-1',
|
||||
spanID: 'start-time-1',
|
||||
},
|
||||
{
|
||||
operationName: 'POST /mything',
|
||||
process: {
|
||||
serviceName: 'alpha',
|
||||
},
|
||||
id: 'start-time-1',
|
||||
spanID: 'start-time-1',
|
||||
},
|
||||
];
|
||||
] as TraceSpan[];
|
||||
|
||||
expect(
|
||||
spanSelectors.filterSpansForText({
|
||||
+24
-22
@@ -15,25 +15,27 @@
|
||||
import fuzzy from 'fuzzy';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { TraceSpan, TraceSpanData, TraceSpanReference } from '../types/trace';
|
||||
|
||||
import { getProcessServiceName } from './process';
|
||||
|
||||
export const getSpanId = (span) => span.spanID;
|
||||
export const getSpanName = (span) => span.operationName;
|
||||
export const getSpanDuration = (span) => span.duration;
|
||||
export const getSpanTimestamp = (span) => span.startTime;
|
||||
export const getSpanProcessId = (span) => span.processID;
|
||||
export const getSpanReferences = (span) => span.references || [];
|
||||
export const getSpanId = (span: TraceSpanData) => span.spanID;
|
||||
export const getSpanName = (span: TraceSpanData) => span.operationName;
|
||||
export const getSpanDuration = (span: TraceSpanData) => span.duration;
|
||||
export const getSpanTimestamp = (span: TraceSpanData) => span.startTime;
|
||||
export const getSpanProcessId = (span: TraceSpanData) => span.processID;
|
||||
export const getSpanReferences = (span: TraceSpanData) => span.references || [];
|
||||
export const getSpanReferenceByType = createSelector(
|
||||
createSelector(({ span }) => span, getSpanReferences),
|
||||
({ type }) => type,
|
||||
(references, type) => references.find((ref) => ref.refType === type)
|
||||
createSelector(({ span }: { span: TraceSpanData }) => span, getSpanReferences),
|
||||
({ type }: { type: string }) => type,
|
||||
(references, type) => references.find((ref: TraceSpanReference) => ref.refType === type)
|
||||
);
|
||||
export const getSpanParentId = createSelector(
|
||||
(span) => getSpanReferenceByType({ span, type: 'CHILD_OF' }),
|
||||
(span: TraceSpanData) => getSpanReferenceByType({ span, type: 'CHILD_OF' }),
|
||||
(childOfRef) => (childOfRef ? childOfRef.spanID : null)
|
||||
);
|
||||
|
||||
export const getSpanProcess = (span) => {
|
||||
export const getSpanProcess = (span: TraceSpan) => {
|
||||
if (!span.process) {
|
||||
throw new Error(
|
||||
`
|
||||
@@ -42,23 +44,22 @@ export const getSpanProcess = (span) => {
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
return span.process;
|
||||
};
|
||||
|
||||
export const getSpanServiceName = createSelector(getSpanProcess, getProcessServiceName);
|
||||
|
||||
export const filterSpansForTimestamps = createSelector(
|
||||
({ spans }) => spans,
|
||||
({ leftBound }) => leftBound,
|
||||
({ rightBound }) => rightBound,
|
||||
({ spans }: { spans: TraceSpanData[] }) => spans,
|
||||
({ leftBound }: { leftBound: number }) => leftBound,
|
||||
({ rightBound }: { rightBound: number }) => rightBound,
|
||||
(spans, leftBound, rightBound) =>
|
||||
spans.filter((span) => getSpanTimestamp(span) >= leftBound && getSpanTimestamp(span) <= rightBound)
|
||||
);
|
||||
|
||||
export const filterSpansForText = createSelector(
|
||||
({ spans }) => spans,
|
||||
({ text }) => text,
|
||||
({ spans }: { spans: TraceSpan[] }) => spans,
|
||||
({ text }: { text: string }) => text,
|
||||
(spans, text) =>
|
||||
fuzzy
|
||||
.filter(text, spans, {
|
||||
@@ -67,7 +68,7 @@ export const filterSpansForText = createSelector(
|
||||
.map(({ original }) => original)
|
||||
);
|
||||
|
||||
const getTextFilterdSpansAsMap = createSelector(filterSpansForText, (matchingSpans) =>
|
||||
const getTextFilteredSpansAsMap = createSelector(filterSpansForText, (matchingSpans) =>
|
||||
matchingSpans.reduce(
|
||||
(obj, span) => ({
|
||||
...obj,
|
||||
@@ -77,11 +78,12 @@ const getTextFilterdSpansAsMap = createSelector(filterSpansForText, (matchingSpa
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: delete this function as it is not used?
|
||||
export const highlightSpansForTextFilter = createSelector(
|
||||
({ spans }) => spans,
|
||||
getTextFilterdSpansAsMap,
|
||||
(spans, textFilteredSpansMap) =>
|
||||
spans.map((span) => ({
|
||||
({ spans }: { spans: TraceSpanData[] }) => spans,
|
||||
getTextFilteredSpansAsMap,
|
||||
(spans, textFilteredSpansMap: { [key: string]: TraceSpanData }) =>
|
||||
spans.map((span: TraceSpanData) => ({
|
||||
...span,
|
||||
muted: !textFilteredSpansMap[getSpanId(span)],
|
||||
}))
|
||||
+11
-7
@@ -14,6 +14,16 @@
|
||||
|
||||
// See https://github.com/jaegertracing/jaeger-ui/issues/115 for details.
|
||||
|
||||
import { TraceSpanReference } from '../types/trace';
|
||||
|
||||
const references: TraceSpanReference[] = [
|
||||
{
|
||||
refType: 'FOLLOWS_FROM',
|
||||
spanID: 'ea7cfaca83f0724b',
|
||||
traceID: '2992f2a5b5d037a8aabffd08ef384237',
|
||||
},
|
||||
];
|
||||
|
||||
export const followsFromRef = {
|
||||
processes: {
|
||||
p1: {
|
||||
@@ -28,13 +38,7 @@ export const followsFromRef = {
|
||||
logs: [],
|
||||
operationName: 'thread',
|
||||
processID: 'p1',
|
||||
references: [
|
||||
{
|
||||
refType: 'FOLLOWS_FROM',
|
||||
spanID: 'ea7cfaca83f0724b',
|
||||
traceID: '2992f2a5b5d037a8aabffd08ef384237',
|
||||
},
|
||||
],
|
||||
references: references,
|
||||
spanID: '1bdf4201221bb2ac',
|
||||
startTime: 1509533706521220,
|
||||
tags: [],
|
||||
+39
-27
@@ -13,8 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { values as _values } from 'lodash';
|
||||
import TreeNode from 'src/utils/TreeNode';
|
||||
|
||||
import traceGenerator from '../demo/trace-generators';
|
||||
import { TraceResponse, TraceSpan, TraceSpanData } from '../types/trace';
|
||||
import { numberSortComparator } from '../utils/sort';
|
||||
|
||||
import {
|
||||
@@ -29,7 +31,7 @@ import {
|
||||
import * as traceSelectors from './trace';
|
||||
import { followsFromRef } from './trace.fixture';
|
||||
|
||||
const generatedTrace = traceGenerator.trace({ numberOfSpans: 45 });
|
||||
const generatedTrace: TraceResponse = traceGenerator.trace({ numberOfSpans: 45 });
|
||||
|
||||
it('getTraceId() should return the traceID', () => {
|
||||
expect(traceSelectors.getTraceId(generatedTrace)).toBe(generatedTrace.traceID);
|
||||
@@ -39,7 +41,7 @@ it('hydrateSpansWithProcesses() should return the trace with processes on each s
|
||||
const hydratedTrace = traceSelectors.hydrateSpansWithProcesses(generatedTrace);
|
||||
|
||||
hydratedTrace.spans.forEach((span) =>
|
||||
expect(getSpanProcess(span)).toBe(generatedTrace.processes[getSpanProcessId(span)])
|
||||
expect(getSpanProcess(span as TraceSpan)).toBe(generatedTrace.processes[getSpanProcessId(span)])
|
||||
);
|
||||
});
|
||||
|
||||
@@ -55,7 +57,7 @@ describe('getTraceSpanIdsAsTree()', () => {
|
||||
const tree = traceSelectors.getTraceSpanIdsAsTree(generatedTrace);
|
||||
const spanMap = traceSelectors.getTraceSpansAsMap(generatedTrace);
|
||||
|
||||
tree.walk((value, node) => {
|
||||
tree.walk((value: string | number | undefined, node: TreeNode) => {
|
||||
const expectedParentValue = value === traceSelectors.TREE_ROOT_ID ? null : value;
|
||||
node.children.forEach((childNode) => {
|
||||
expect(getSpanParentId(spanMap.get(childNode.value))).toBe(expectedParentValue);
|
||||
@@ -98,7 +100,7 @@ it('getParentSpan() should return the first span if there are multiple parents',
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
} as unknown as TraceResponse;
|
||||
|
||||
expect(traceSelectors.getParentSpan(trace)).toBe(firstSpan);
|
||||
});
|
||||
@@ -131,14 +133,14 @@ it('getTraceDepth() should determine the total depth of the trace tree', () => {
|
||||
});
|
||||
|
||||
it('getSpanDepthForTrace() should determine the depth of a given span in the parent', () => {
|
||||
function testDepthCalc(span) {
|
||||
function testDepthCalc(span: TraceSpanData) {
|
||||
let depth = 2;
|
||||
let currentId = getSpanParentId(span);
|
||||
|
||||
const findCurrentSpanById = (item) => getSpanId(item) === currentId;
|
||||
const findCurrentSpanById = (item: TraceSpanData) => getSpanId(item) === currentId;
|
||||
while (currentId !== getSpanId(generatedTrace.spans[0])) {
|
||||
depth++;
|
||||
currentId = getSpanParentId(generatedTrace.spans.find(findCurrentSpanById));
|
||||
currentId = getSpanParentId(generatedTrace.spans.find(findCurrentSpanById)!);
|
||||
}
|
||||
|
||||
// console.log('hypothetical depth', depth);
|
||||
@@ -183,10 +185,13 @@ it('formatDurationForUnit() should use the formatters to return the proper value
|
||||
});
|
||||
|
||||
it('formatDurationForTrace() should return a ms value for traces shorter than a second', () => {
|
||||
const firstSpan = generatedTrace.spans[0];
|
||||
firstSpan.duration = 600000;
|
||||
expect(
|
||||
traceSelectors.formatDurationForTrace({
|
||||
trace: {
|
||||
spans: [{ duration: 600000 }],
|
||||
...generatedTrace,
|
||||
spans: [firstSpan],
|
||||
},
|
||||
duration: 302000,
|
||||
})
|
||||
@@ -266,14 +271,17 @@ it('getTreeSizeForTraceSpan() should return the size for a child span', () => {
|
||||
trace: generatedTrace,
|
||||
span: generatedTrace.spans[1],
|
||||
})
|
||||
).toBe(traceSelectors.getTraceSpanIdsAsTree(generatedTrace).find(generatedTrace.spans[1].spanID).size - 1);
|
||||
).toBe(traceSelectors.getTraceSpanIdsAsTree(generatedTrace).find(generatedTrace.spans[1].spanID)!.size - 1);
|
||||
});
|
||||
|
||||
it('getTreeSizeForTraceSpan() should return -1 for an absent span', () => {
|
||||
const absentSpan = generatedTrace.spans[0];
|
||||
absentSpan.spanID = 'whatever';
|
||||
|
||||
expect(
|
||||
traceSelectors.getTreeSizeForTraceSpan({
|
||||
trace: generatedTrace,
|
||||
span: { spanID: 'whatever' },
|
||||
span: absentSpan,
|
||||
})
|
||||
).toBe(-1);
|
||||
});
|
||||
@@ -287,7 +295,7 @@ it('getTraceName() should return the trace name based on the parentSpan', () =>
|
||||
|
||||
it('omitCollapsedSpans() should filter out collapsed spans', () => {
|
||||
const span = generatedTrace.spans[1];
|
||||
const size = traceSelectors.getTraceSpanIdsAsTree(generatedTrace).find(span.spanID).size - 1;
|
||||
const size = traceSelectors.getTraceSpanIdsAsTree(generatedTrace).find(span.spanID)!.size - 1;
|
||||
|
||||
expect(
|
||||
traceSelectors.omitCollapsedSpans({
|
||||
@@ -299,15 +307,13 @@ it('omitCollapsedSpans() should filter out collapsed spans', () => {
|
||||
});
|
||||
|
||||
it('getTicksForTrace() should return a list of ticks given interval parameters', () => {
|
||||
const trace = generatedTrace;
|
||||
const timestamp = new Date().getTime() * 1000;
|
||||
const trace = {
|
||||
spans: [
|
||||
{
|
||||
startTime: timestamp,
|
||||
duration: 3000000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
trace.spans.forEach((span) => {
|
||||
span.duration = 3000000;
|
||||
span.startTime = timestamp;
|
||||
});
|
||||
|
||||
expect(
|
||||
traceSelectors.getTicksForTrace({
|
||||
@@ -325,14 +331,20 @@ it('getTicksForTrace() should return a list of ticks given interval parameters',
|
||||
|
||||
it('getTicksForTrace() should use defaults', () => {
|
||||
const timestamp = new Date().getTime() * 1000;
|
||||
const trace = {
|
||||
spans: [
|
||||
{
|
||||
startTime: timestamp,
|
||||
duration: 4000000,
|
||||
},
|
||||
],
|
||||
};
|
||||
const trace = traceGenerator.trace({ numberOfSpans: 1 });
|
||||
|
||||
trace.spans = [
|
||||
{
|
||||
traceID: '5031233a-d0b5-5d41-9b4b-4c072bcf5020',
|
||||
processID: 'b5f4e0ff-7318-5017-a3f3-9c7b423a82aa',
|
||||
spanID: 'e871771f-f1b4-54af-9e9d-826259c2915e',
|
||||
flags: 0,
|
||||
logs: [],
|
||||
operationName: 'POST',
|
||||
startTime: timestamp,
|
||||
duration: 4000000,
|
||||
},
|
||||
];
|
||||
|
||||
expect(traceSelectors.getTicksForTrace({ trace })).toEqual([
|
||||
{ timestamp, width: traceSelectors.DEFAULT_TICK_WIDTH },
|
||||
+75
-63
@@ -14,6 +14,7 @@
|
||||
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
|
||||
import { Trace, TraceData, TraceProcess, TraceResponse, TraceSpan, TraceSpanData } from '../types/trace';
|
||||
import TreeNode from '../utils/TreeNode';
|
||||
import { formatMillisecondTime, formatSecondTime, ONE_SECOND } from '../utils/date';
|
||||
import { numberSortComparator } from '../utils/sort';
|
||||
@@ -28,15 +29,13 @@ import {
|
||||
getSpanProcessId,
|
||||
} from './span';
|
||||
|
||||
export const getTraceId = (trace) => trace.traceID;
|
||||
|
||||
export const getTraceSpans = (trace) => trace.spans;
|
||||
|
||||
const getTraceProcesses = (trace) => trace.processes;
|
||||
export const getTraceId = (trace: TraceData) => trace.traceID;
|
||||
export const getTraceSpans = (trace: TraceResponse) => trace.spans;
|
||||
const getTraceProcesses = (trace: TraceData | Trace) => trace.processes;
|
||||
|
||||
const getSpanWithProcess = createSelector(
|
||||
(state) => state.span,
|
||||
(state) => state.processes,
|
||||
(state: { span: TraceSpanData; processes: Record<string, TraceProcess> }) => state.span,
|
||||
(state: { span: TraceSpanData; processes: Record<string, TraceProcess> }) => state.processes,
|
||||
(span, processes) => ({
|
||||
...span,
|
||||
process: processes[getSpanProcessId(span)],
|
||||
@@ -44,7 +43,7 @@ const getSpanWithProcess = createSelector(
|
||||
);
|
||||
|
||||
export const getTraceSpansAsMap = createSelector(getTraceSpans, (spans) =>
|
||||
spans.reduce((map, span) => map.set(getSpanId(span), span), new Map())
|
||||
spans.reduce((map, span: TraceSpanData) => map.set(getSpanId(span), span), new Map())
|
||||
);
|
||||
|
||||
export const TREE_ROOT_ID = '__root__';
|
||||
@@ -63,17 +62,17 @@ export const TREE_ROOT_ID = '__root__';
|
||||
* @return {TreeNode} A tree of spanIDs derived from the relationships
|
||||
* between spans in the trace.
|
||||
*/
|
||||
export function getTraceSpanIdsAsTree(trace) {
|
||||
const nodesById = new Map(trace.spans.map((span) => [span.spanID, new TreeNode(span.spanID)]));
|
||||
const spansById = new Map(trace.spans.map((span) => [span.spanID, span]));
|
||||
export function getTraceSpanIdsAsTree(trace: TraceResponse) {
|
||||
const nodesById = new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, new TreeNode(span.spanID)]));
|
||||
const spansById = new Map(trace.spans.map((span: TraceSpanData) => [span.spanID, span]));
|
||||
const root = new TreeNode(TREE_ROOT_ID);
|
||||
trace.spans.forEach((span) => {
|
||||
const node = nodesById.get(span.spanID);
|
||||
trace.spans.forEach((span: TraceSpanData) => {
|
||||
const node = nodesById.get(span.spanID)!;
|
||||
if (Array.isArray(span.references) && span.references.length) {
|
||||
const { refType, spanID: parentID } = span.references[0];
|
||||
if (refType === 'CHILD_OF' || refType === 'FOLLOWS_FROM') {
|
||||
const parent = nodesById.get(parentID) || root;
|
||||
parent.children.push(node);
|
||||
parent.children?.push(node);
|
||||
} else {
|
||||
throw new Error(`Unrecognized ref type: ${refType}`);
|
||||
}
|
||||
@@ -81,15 +80,15 @@ export function getTraceSpanIdsAsTree(trace) {
|
||||
root.children.push(node);
|
||||
}
|
||||
});
|
||||
const comparator = (nodeA, nodeB) => {
|
||||
const a = spansById.get(nodeA.value);
|
||||
const b = spansById.get(nodeB.value);
|
||||
return +(a.startTime > b.startTime) || +(a.startTime === b.startTime) - 1;
|
||||
const comparator = (nodeA: TreeNode | undefined, nodeB: TreeNode | undefined) => {
|
||||
const a: TraceSpanData | undefined = nodeA?.value ? spansById.get(nodeA.value.toString()) : undefined;
|
||||
const b: TraceSpanData | undefined = nodeB?.value ? spansById.get(nodeB.value.toString()) : undefined;
|
||||
return +(a?.startTime! > b?.startTime!) || +(a?.startTime === b?.startTime) - 1;
|
||||
};
|
||||
trace.spans.forEach((span) => {
|
||||
const node = nodesById.get(span.spanID);
|
||||
if (node.children.length > 1) {
|
||||
node.children.sort(comparator);
|
||||
trace.spans.forEach((span: TraceSpanData) => {
|
||||
const node: TreeNode | undefined = nodesById.get(span.spanID);
|
||||
if (node!.children.length > 1) {
|
||||
node?.children.sort(comparator);
|
||||
}
|
||||
});
|
||||
root.children.sort(comparator);
|
||||
@@ -97,13 +96,13 @@ export function getTraceSpanIdsAsTree(trace) {
|
||||
}
|
||||
|
||||
// attach "process" as an object to each span.
|
||||
export const hydrateSpansWithProcesses = (trace) => {
|
||||
export const hydrateSpansWithProcesses = (trace: TraceResponse) => {
|
||||
const spans = getTraceSpans(trace);
|
||||
const processes = getTraceProcesses(trace);
|
||||
|
||||
return {
|
||||
...trace,
|
||||
spans: spans.map((span) => getSpanWithProcess({ span, processes })),
|
||||
spans: spans.map((span: TraceSpanData) => getSpanWithProcess({ span, processes })),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -111,25 +110,26 @@ export const getTraceSpanCount = createSelector(getTraceSpans, (spans) => spans.
|
||||
|
||||
export const getTraceTimestamp = createSelector(getTraceSpans, (spans) =>
|
||||
spans.reduce(
|
||||
(prevTimestamp, span) => (prevTimestamp ? Math.min(prevTimestamp, getSpanTimestamp(span)) : getSpanTimestamp(span)),
|
||||
null
|
||||
(prevTimestamp: number, span: TraceSpanData) =>
|
||||
prevTimestamp ? Math.min(prevTimestamp, getSpanTimestamp(span)) : getSpanTimestamp(span),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
export const getTraceDuration = createSelector(getTraceSpans, getTraceTimestamp, (spans, timestamp) =>
|
||||
spans.reduce(
|
||||
(prevDuration, span) =>
|
||||
(prevDuration: number, span: TraceSpanData) =>
|
||||
prevDuration
|
||||
? Math.max(getSpanTimestamp(span) - timestamp + getSpanDuration(span), prevDuration)
|
||||
? Math.max(getSpanTimestamp(span) - timestamp! + getSpanDuration(span), prevDuration)
|
||||
: getSpanDuration(span),
|
||||
null
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
export const getTraceEndTimestamp = createSelector(
|
||||
getTraceTimestamp,
|
||||
getTraceDuration,
|
||||
(timestamp, duration) => timestamp + duration
|
||||
(timestamp: number, duration: number) => timestamp! + duration
|
||||
);
|
||||
|
||||
export const getParentSpan = createSelector(
|
||||
@@ -137,16 +137,18 @@ export const getParentSpan = createSelector(
|
||||
getTraceSpansAsMap,
|
||||
(tree, spanMap) =>
|
||||
tree.children
|
||||
.map((node) => spanMap.get(node.value))
|
||||
.sort((spanA, spanB) => numberSortComparator(getSpanTimestamp(spanA), getSpanTimestamp(spanB)))[0]
|
||||
.map((node: TreeNode) => spanMap.get(node.value))
|
||||
.sort((spanA: TraceSpanData, spanB: TraceSpanData) =>
|
||||
numberSortComparator(getSpanTimestamp(spanA), getSpanTimestamp(spanB))
|
||||
)[0]
|
||||
);
|
||||
|
||||
export const getTraceDepth = createSelector(getTraceSpanIdsAsTree, (spanTree) => spanTree.depth - 1);
|
||||
|
||||
export const getSpanDepthForTrace = createSelector(
|
||||
createSelector((state) => state.trace, getTraceSpanIdsAsTree),
|
||||
createSelector((state) => state.span, getSpanId),
|
||||
(node, spanID) => node.getPath(spanID).length - 1
|
||||
createSelector((state: { trace: TraceResponse }) => state.trace, getTraceSpanIdsAsTree),
|
||||
createSelector((state: { span: TraceSpanData }) => state.span, getSpanId),
|
||||
(node, spanID) => node.getPath(spanID)!.length - 1
|
||||
);
|
||||
|
||||
export const getTraceServices = createSelector(getTraceProcesses, (processes) =>
|
||||
@@ -165,26 +167,34 @@ export const DURATION_FORMATTERS = {
|
||||
s: formatSecondTime,
|
||||
};
|
||||
|
||||
const getDurationFormatterForTrace = createSelector(getTraceDuration, (totalDuration) =>
|
||||
const getDurationFormatterForTrace = createSelector(getTraceDuration, (totalDuration: number) =>
|
||||
totalDuration >= ONE_SECOND ? DURATION_FORMATTERS.s : DURATION_FORMATTERS.ms
|
||||
);
|
||||
|
||||
export const formatDurationForUnit = createSelector(
|
||||
({ duration }) => duration,
|
||||
({ unit }) => DURATION_FORMATTERS[unit],
|
||||
({ duration }: { duration: number }) => duration,
|
||||
({ unit }: { unit: 'ms' | 's' }) => DURATION_FORMATTERS[unit],
|
||||
(duration, formatter) => formatter(duration)
|
||||
);
|
||||
|
||||
export const formatDurationForTrace = createSelector(
|
||||
({ duration }) => duration,
|
||||
createSelector(({ trace }) => trace, getDurationFormatterForTrace),
|
||||
({ duration }: { duration: number }) => duration,
|
||||
createSelector(({ trace }: { trace: TraceResponse }) => trace, getDurationFormatterForTrace),
|
||||
(duration, formatter) => formatter(duration)
|
||||
);
|
||||
|
||||
export const getSortedSpans = createSelector(
|
||||
({ trace }) => trace,
|
||||
({ spans }) => spans,
|
||||
({ sort }) => sort,
|
||||
({ trace }: { trace: TraceResponse }) => trace,
|
||||
({ spans }: { spans: TraceSpanData[] }) => spans,
|
||||
({
|
||||
sort,
|
||||
}: {
|
||||
sort: {
|
||||
dir: number;
|
||||
comparator: (itemA: number, itemB: number) => number;
|
||||
selector: (itemA: TraceSpanData, itemB: TraceResponse) => number;
|
||||
};
|
||||
}) => sort,
|
||||
(trace, spans, { dir, comparator, selector }) =>
|
||||
[...spans].sort((spanA, spanB) => dir * comparator(selector(spanA, trace), selector(spanB, trace)))
|
||||
);
|
||||
@@ -192,13 +202,13 @@ export const getSortedSpans = createSelector(
|
||||
const getTraceSpansByHierarchyPosition = createSelector(getTraceSpanIdsAsTree, (tree) => {
|
||||
const hierarchyPositionMap = new Map();
|
||||
let i = 0;
|
||||
tree.walk((spanID) => hierarchyPositionMap.set(spanID, i++));
|
||||
tree.walk((spanID: string | number | undefined) => hierarchyPositionMap.set(spanID, i++));
|
||||
return hierarchyPositionMap;
|
||||
});
|
||||
|
||||
export const getTreeSizeForTraceSpan = createSelector(
|
||||
createSelector((state) => state.trace, getTraceSpanIdsAsTree),
|
||||
createSelector((state) => state.span, getSpanId),
|
||||
createSelector((state: { trace: TraceResponse }) => state.trace, getTraceSpanIdsAsTree),
|
||||
createSelector((state: { span: TraceSpanData }) => state.span, getSpanId),
|
||||
(tree, spanID) => {
|
||||
const node = tree.find(spanID);
|
||||
if (!node) {
|
||||
@@ -209,8 +219,8 @@ export const getTreeSizeForTraceSpan = createSelector(
|
||||
);
|
||||
|
||||
export const getSpanHierarchySortPositionForTrace = createSelector(
|
||||
createSelector(({ trace }) => trace, getTraceSpansByHierarchyPosition),
|
||||
({ span }) => span,
|
||||
createSelector(({ trace }: { trace: Trace }) => trace, getTraceSpansByHierarchyPosition),
|
||||
({ span }: { span: TraceSpan }) => span,
|
||||
(hierarchyPositionMap, span) => hierarchyPositionMap.get(getSpanId(span))
|
||||
);
|
||||
|
||||
@@ -222,16 +232,16 @@ export const getTraceName = createSelector(
|
||||
serviceName: getSpanServiceName,
|
||||
})
|
||||
),
|
||||
({ name, serviceName }) => `${serviceName}: ${name}`
|
||||
({ name, serviceName }: { name: string; serviceName: string }) => `${serviceName}: ${name}`
|
||||
);
|
||||
|
||||
export const omitCollapsedSpans = createSelector(
|
||||
({ spans }) => spans,
|
||||
createSelector(({ trace }) => trace, getTraceSpanIdsAsTree),
|
||||
({ collapsed }) => collapsed,
|
||||
({ spans }: { spans: TraceSpanData[] }) => spans,
|
||||
createSelector(({ trace }: { trace: TraceResponse }) => trace, getTraceSpanIdsAsTree),
|
||||
({ collapsed }: { collapsed: string[] }) => collapsed,
|
||||
(spans, tree, collapse) => {
|
||||
const hiddenSpanIds = collapse.reduce((result, collapsedSpanId) => {
|
||||
tree.find(collapsedSpanId).walk((id) => id !== collapsedSpanId && result.add(id));
|
||||
tree.find(collapsedSpanId)!.walk((id: string | number | undefined) => id !== collapsedSpanId && result.add(id));
|
||||
return result;
|
||||
}, new Set());
|
||||
|
||||
@@ -242,13 +252,13 @@ export const omitCollapsedSpans = createSelector(
|
||||
export const DEFAULT_TICK_INTERVAL = 4;
|
||||
export const DEFAULT_TICK_WIDTH = 3;
|
||||
export const getTicksForTrace = createSelector(
|
||||
({ trace }) => trace,
|
||||
({ interval = DEFAULT_TICK_INTERVAL }) => interval,
|
||||
({ width = DEFAULT_TICK_WIDTH }) => width,
|
||||
({ trace }: { trace: TraceResponse }) => trace,
|
||||
({ interval = DEFAULT_TICK_INTERVAL }: { interval?: number }) => interval,
|
||||
({ width = DEFAULT_TICK_WIDTH }: { width?: number }) => width,
|
||||
(
|
||||
trace,
|
||||
interval,
|
||||
width
|
||||
interval: number,
|
||||
width: number
|
||||
// timestamps will be spaced over the interval, starting from the initial timestamp
|
||||
) =>
|
||||
[...Array(interval + 1).keys()].map((num) => ({
|
||||
@@ -260,14 +270,16 @@ export const getTicksForTrace = createSelector(
|
||||
// TODO: delete this when the backend can ensure uniqueness
|
||||
/* istanbul ignore next */
|
||||
export const enforceUniqueSpanIds = createSelector(
|
||||
/* istanbul ignore next */ (trace) => trace,
|
||||
/* istanbul ignore next */ (trace: Trace) => trace,
|
||||
getTraceSpans,
|
||||
/* istanbul ignore next */ (trace, spans) => {
|
||||
const map = new Map();
|
||||
|
||||
const spanArray: TraceSpanData[] = [];
|
||||
|
||||
return {
|
||||
...trace,
|
||||
spans: spans.reduce((result, span) => {
|
||||
spans: spans.reduce((result: TraceSpanData[], span: TraceSpanData) => {
|
||||
const spanID = map.has(getSpanId(span)) ? `${getSpanId(span)}_${map.get(getSpanId(span))}` : getSpanId(span);
|
||||
const updatedSpan = { ...span, spanID };
|
||||
|
||||
@@ -280,17 +292,17 @@ export const enforceUniqueSpanIds = createSelector(
|
||||
map.set(getSpanId(span), (map.get(getSpanId(span)) || 0) + 1);
|
||||
|
||||
return result.concat([updatedSpan]);
|
||||
}, []),
|
||||
}, spanArray),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: delete this when the backend can ensure uniqueness
|
||||
export const dropEmptyStartTimeSpans = createSelector(
|
||||
/* istanbul ignore next */ (trace) => trace,
|
||||
/* istanbul ignore next */ (trace: Trace) => trace,
|
||||
getTraceSpans,
|
||||
/* istanbul ignore next */ (trace, spans) => ({
|
||||
...trace,
|
||||
spans: spans.filter((span) => !!getSpanTimestamp(span)),
|
||||
spans: spans.filter((span: TraceSpanData) => !!getSpanTimestamp(span)),
|
||||
})
|
||||
);
|
||||
@@ -22,7 +22,7 @@ export default class TreeNode {
|
||||
return (node: TreeNode) => fn(node.value, node, depth);
|
||||
}
|
||||
|
||||
static searchFunction(search: TreeNode | number | SearchFn) {
|
||||
static searchFunction(search: TreeNode | number | SearchFn | string) {
|
||||
if (typeof search === 'function') {
|
||||
return search;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export default class TreeNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
find(search: TreeNode | number | SearchFn): TreeNode | null {
|
||||
find(search: TreeNode | number | SearchFn | string): TreeNode | null {
|
||||
const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search));
|
||||
if (searchFn(this)) {
|
||||
return this;
|
||||
@@ -65,7 +65,7 @@ export default class TreeNode {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPath(search: TreeNode) {
|
||||
getPath(search: TreeNode | string) {
|
||||
const searchFn = TreeNode.iterFunction(TreeNode.searchFunction(search));
|
||||
|
||||
const findPath = (currentNode: TreeNode, currentPath: TreeNode[]): TreeNode[] | null => {
|
||||
|
||||
@@ -26,7 +26,7 @@ it('gives the same color for the same key', () => {
|
||||
expect(colorOne).toBe(colorTwo);
|
||||
});
|
||||
|
||||
it('gives different colors for each for each key', () => {
|
||||
it('gives different colors for each key', () => {
|
||||
clear();
|
||||
const colorOne = getColorByKey('serviceA', createTheme());
|
||||
const colorTwo = getColorByKey('serviceB', createTheme());
|
||||
@@ -35,8 +35,10 @@ it('gives different colors for each for each key', () => {
|
||||
|
||||
it('should not allow red', () => {
|
||||
expect(colorsToFilter.indexOf('#E24D42')).toBe(4);
|
||||
expect(colorsToFilter.indexOf('#BF1B00')).toBe(28);
|
||||
const filteredColors = getFilteredColors(colorsToFilter, createTheme());
|
||||
expect(filteredColors.indexOf('#E24D42')).toBe(-1);
|
||||
expect(filteredColors.indexOf('#BF1B00')).toBe(-1);
|
||||
});
|
||||
|
||||
it('should not allow colors with a contrast ratio < 3 in light mode', () => {
|
||||
@@ -54,3 +56,23 @@ it('should not allow colors with a contrast ratio < 3 in dark mode', () => {
|
||||
expect(filteredColors.indexOf('#890F02')).toBe(-1);
|
||||
expect(filteredColors.indexOf('#0A437C')).toBe(-1);
|
||||
});
|
||||
|
||||
it('should not allow a color that is the same as the previous color', () => {
|
||||
clear();
|
||||
const theme = createTheme();
|
||||
const colorOne = getColorByKey('random4', theme); // #447EBC
|
||||
const colorTwo = getColorByKey('random17', theme); // #447EBC
|
||||
expect(colorOne).not.toBe(colorTwo);
|
||||
expect(colorOne).toBe('#447EBC');
|
||||
expect(colorTwo).toBe('#B7DBAB');
|
||||
});
|
||||
|
||||
it('should not allow a color that looks similar to the previous color', () => {
|
||||
clear();
|
||||
const theme = createTheme({ colors: { mode: 'light' } });
|
||||
const colorOne = getColorByKey('random9', theme); // #58140C
|
||||
const colorTwo = getColorByKey('random18', theme); // #511749
|
||||
expect(colorOne).toBe('#58140C');
|
||||
// #1F78C1 is the next color that has a contrast ratio >= 3 for the current theme
|
||||
expect(colorTwo).toBe('#1F78C1');
|
||||
});
|
||||
|
||||
@@ -45,13 +45,40 @@ class ColorGenerator {
|
||||
let i = this.cache.get(key);
|
||||
if (i == null) {
|
||||
const hash = this.hashCode(key.toLowerCase());
|
||||
const hashIndex = Math.abs(hash % this.colorsHex.length);
|
||||
i = hashIndex === 4 ? hashIndex + 1 : hashIndex;
|
||||
i = Math.abs(hash % this.colorsHex.length);
|
||||
|
||||
const prevCacheItem = Array.from(this.cache).pop();
|
||||
if (prevCacheItem && prevCacheItem[1]) {
|
||||
// disallow a color that is the same as the previous color
|
||||
if (prevCacheItem[1] === i) {
|
||||
i = this.getNextIndex(i);
|
||||
}
|
||||
|
||||
// disallow a color that looks very similar to the previous color
|
||||
const prevColor = this.colorsHex[prevCacheItem[1]];
|
||||
if (tinycolor.readability(prevColor, this.colorsHex[i]) < 1.5) {
|
||||
let newIndex = i;
|
||||
for (let j = 0; j < this.colorsHex.length; j++) {
|
||||
newIndex = this.getNextIndex(newIndex);
|
||||
|
||||
if (tinycolor.readability(prevColor, this.colorsHex[newIndex]) > 1.5) {
|
||||
i = newIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cache.set(key, i);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
getNextIndex(i: number) {
|
||||
// get next index or go back to 0
|
||||
return i + 1 < this.colorsHex.length ? i + 1 : 0;
|
||||
}
|
||||
|
||||
hashCode(key: string) {
|
||||
let hash = 0,
|
||||
i,
|
||||
@@ -110,6 +137,10 @@ export function getFilteredColors(colorsHex: string[], theme: GrafanaTheme2) {
|
||||
if (redIndex > -1) {
|
||||
colorsHex.splice(redIndex, 1);
|
||||
}
|
||||
const redIndex2 = colorsHex.indexOf('#BF1B00');
|
||||
if (redIndex2 > -1) {
|
||||
colorsHex.splice(redIndex2, 1);
|
||||
}
|
||||
|
||||
// Only add colors that have a contrast ratio >= 3 for the current theme
|
||||
let filteredColors = [];
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
ARG BASE_IMAGE=alpine:3.15
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG GRAFANA_TGZ="grafana-latest.linux-x64-musl.tar.gz"
|
||||
|
||||
# Make sure we have Gnu tar
|
||||
RUN apk add --no-cache tar
|
||||
|
||||
COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
|
||||
|
||||
# Change to tar xfzv to make tar print every file it extracts
|
||||
RUN mkdir /tmp/grafana && tar xzf /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG GF_UID="472"
|
||||
ARG GF_GID="0"
|
||||
|
||||
ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
|
||||
GF_PATHS_DATA="/var/lib/grafana" \
|
||||
GF_PATHS_HOME="/usr/share/grafana" \
|
||||
GF_PATHS_LOGS="/var/log/grafana" \
|
||||
GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
|
||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
RUN apk add --no-cache ca-certificates bash tzdata musl-utils
|
||||
RUN apk info -vv | sort
|
||||
|
||||
# Oracle Support for x86_64 only
|
||||
RUN if [ `arch` = "x86_64" ]; then \
|
||||
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-2.35-r0.apk \
|
||||
-O /tmp/glibc-2.35-r0.apk && \
|
||||
wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.35-r0/glibc-bin-2.35-r0.apk \
|
||||
-O /tmp/glibc-bin-2.35-r0.apk && \
|
||||
apk add --no-cache --allow-untrusted /tmp/glibc-2.35-r0.apk /tmp/glibc-bin-2.35-r0.apk && \
|
||||
rm -f /lib64/ld-linux-x86-64.so.2 && \
|
||||
ln -s /usr/glibc-compat/lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 && \
|
||||
rm -f /tmp/glibc-2.35-r0.apk && \
|
||||
rm -f /tmp/glibc-bin-2.35-r0.apk && \
|
||||
rm -f /lib/ld-linux-x86-64.so.2 && \
|
||||
rm -f /etc/ld.so.cache; \
|
||||
fi
|
||||
|
||||
COPY --from=0 /tmp/grafana "$GF_PATHS_HOME"
|
||||
|
||||
RUN if [ ! $(getent group "$GF_GID") ]; then \
|
||||
addgroup -S -g $GF_GID grafana; \
|
||||
fi
|
||||
|
||||
RUN export GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \
|
||||
mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
adduser -S -u $GF_UID -G "$GF_GID_NAME" grafana && \
|
||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
||||
"$GF_PATHS_PROVISIONING/plugins" \
|
||||
"$GF_PATHS_PROVISIONING/access-control" \
|
||||
"$GF_PATHS_PROVISIONING/alerting" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
|
||||
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
|
||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
COPY ./run.sh /run.sh
|
||||
|
||||
USER "$GF_UID"
|
||||
ENTRYPOINT [ "/run.sh" ]
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
UBUNTU_BASE=0
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case "$1" in
|
||||
"--ubuntu")
|
||||
UBUNTU_BASE=1
|
||||
echo "Ubuntu base image enabled"
|
||||
shift
|
||||
;;
|
||||
* )
|
||||
# unknown param causes args to be passed through to $@
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_raw_grafana_tag=$1
|
||||
_docker_repo=${2:-grafana/grafana-enterprise}
|
||||
|
||||
if echo "$_raw_grafana_tag" | grep -q "^v"; then
|
||||
_grafana_tag=$(echo "${_raw_grafana_tag}" | cut -d "v" -f 2)
|
||||
elif echo "$_raw_grafana_tag" | grep -q "^main-"; then
|
||||
_grafana_tag="main"
|
||||
else
|
||||
_grafana_tag="${_raw_grafana_tag}"
|
||||
fi
|
||||
|
||||
if [ ${UBUNTU_BASE} = "0" ]; then
|
||||
TAG_SUFFIX=""
|
||||
DOCKERFILE="Dockerfile"
|
||||
else
|
||||
TAG_SUFFIX="-ubuntu"
|
||||
DOCKERFILE="ubuntu.Dockerfile"
|
||||
fi
|
||||
|
||||
echo "Building and deploying ${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}"
|
||||
|
||||
docker build \
|
||||
--tag "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}" \
|
||||
--no-cache=true \
|
||||
-f ${DOCKERFILE} \
|
||||
.
|
||||
|
||||
docker push "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}"
|
||||
|
||||
if echo "$_raw_grafana_tag" | grep -q "^v" && echo "$_raw_grafana_tag" | grep -qv "beta"; then
|
||||
docker tag "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}" "${_docker_repo}:latest${TAG_SUFFIX}"
|
||||
docker push "${_docker_repo}:latest${TAG_SUFFIX}"
|
||||
fi
|
||||
|
||||
|
||||
if echo "${_raw_grafana_tag}" | grep -q "^main-" && [ ${UBUNTU_BASE} = "1" ]; then
|
||||
docker tag "${_docker_repo}:${_grafana_tag}${TAG_SUFFIX}" "grafana/grafana-enterprise-dev:${_raw_grafana_tag}"
|
||||
docker push "grafana/grafana-enterprise-dev:${_raw_grafana_tag}"
|
||||
fi
|
||||
|
||||
@@ -28,7 +28,7 @@ done
|
||||
_grafana_tag=${1:-}
|
||||
_docker_repo=${2:-grafana/grafana}
|
||||
|
||||
# If the tag starts with v, treat this as a official release
|
||||
# If the tag starts with v, treat this as an official release
|
||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
||||
else
|
||||
@@ -59,23 +59,25 @@ docker_build () {
|
||||
esac
|
||||
if [ $UBUNTU_BASE = "0" ]; then
|
||||
libc="-musl"
|
||||
dockerfile="Dockerfile"
|
||||
base_image="${base_arch}alpine:3.15"
|
||||
else
|
||||
libc=""
|
||||
dockerfile="ubuntu.Dockerfile"
|
||||
base_image="${base_arch}ubuntu:20.04"
|
||||
fi
|
||||
|
||||
grafana_tgz="grafana-latest.linux-${arch}${libc}.tar.gz"
|
||||
grafana_tgz=${GRAFANA_TGZ:-"grafana-latest.linux-${arch}${libc}.tar.gz"}
|
||||
tag="${_docker_repo}${repo_arch}:${_grafana_version}${TAG_SUFFIX}"
|
||||
|
||||
DOCKER_BUILDKIT=1 \
|
||||
docker build \
|
||||
--build-arg BASE_IMAGE=${base_image} \
|
||||
--build-arg GRAFANA_TGZ=${grafana_tgz} \
|
||||
--build-arg GO_SRC=tgz-builder \
|
||||
--build-arg JS_SRC=tgz-builder \
|
||||
--build-arg RUN_SH=./run.sh \
|
||||
--tag "${tag}" \
|
||||
--no-cache=true \
|
||||
-f "${dockerfile}" \
|
||||
--file ../../Dockerfile \
|
||||
.
|
||||
}
|
||||
|
||||
|
||||
@@ -1,48 +1,44 @@
|
||||
ARG GRAFANA_VERSION="latest"
|
||||
|
||||
FROM grafana/grafana:${GRAFANA_VERSION}
|
||||
|
||||
USER root
|
||||
|
||||
ARG GF_INSTALL_IMAGE_RENDERER_PLUGIN="false"
|
||||
|
||||
ARG GF_GID="0"
|
||||
ENV GF_PATHS_PLUGINS="/var/lib/grafana-plugins"
|
||||
|
||||
RUN mkdir -p "$GF_PATHS_PLUGINS" && \
|
||||
chown -R grafana:${GF_GID} "$GF_PATHS_PLUGINS"
|
||||
|
||||
RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
apk --no-cache upgrade && \
|
||||
apk add --no-cache udev ttf-opensans chromium && \
|
||||
rm -rf /tmp/* && \
|
||||
rm -rf /usr/share/grafana/tools/phantomjs; \
|
||||
fi
|
||||
|
||||
USER grafana
|
||||
|
||||
ENV GF_PLUGIN_RENDERING_CHROME_BIN="/usr/bin/chromium-browser"
|
||||
|
||||
RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
grafana-cli \
|
||||
--pluginsDir "$GF_PATHS_PLUGINS" \
|
||||
--pluginUrl https://github.com/grafana/grafana-image-renderer/releases/latest/download/plugin-linux-x64-glibc-no-chromium.zip \
|
||||
plugins install grafana-image-renderer; \
|
||||
fi
|
||||
|
||||
ARG GF_INSTALL_PLUGINS=""
|
||||
|
||||
RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \
|
||||
OLDIFS=$IFS; \
|
||||
IFS=','; \
|
||||
for plugin in ${GF_INSTALL_PLUGINS}; do \
|
||||
IFS=$OLDIFS; \
|
||||
if expr match "$plugin" '.*\;.*'; then \
|
||||
pluginUrl=$(echo "$plugin" | cut -d';' -f 1); \
|
||||
pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2); \
|
||||
grafana-cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}"; \
|
||||
else \
|
||||
grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}; \
|
||||
fi \
|
||||
done \
|
||||
fi
|
||||
ARG GRAFANA_VERSION="latest"
|
||||
|
||||
FROM grafana/grafana:${GRAFANA_VERSION}
|
||||
|
||||
USER root
|
||||
|
||||
ARG GF_INSTALL_IMAGE_RENDERER_PLUGIN="false"
|
||||
|
||||
ARG GF_GID="0"
|
||||
ENV GF_PATHS_PLUGINS="/var/lib/grafana-plugins"
|
||||
|
||||
RUN mkdir -p "$GF_PATHS_PLUGINS" && \
|
||||
chown -R grafana:${GF_GID} "$GF_PATHS_PLUGINS" && \
|
||||
if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
apk add --no-cache udev ttf-opensans chromium \
|
||||
fi
|
||||
|
||||
USER grafana
|
||||
|
||||
ENV GF_PLUGIN_RENDERING_CHROME_BIN="/usr/bin/chromium-browser"
|
||||
|
||||
RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
grafana-cli \
|
||||
--pluginsDir "$GF_PATHS_PLUGINS" \
|
||||
--pluginUrl https://github.com/grafana/grafana-image-renderer/releases/latest/download/plugin-linux-x64-glibc-no-chromium.zip \
|
||||
plugins install grafana-image-renderer; \
|
||||
fi
|
||||
|
||||
ARG GF_INSTALL_PLUGINS=""
|
||||
|
||||
RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \
|
||||
OLDIFS=$IFS; \
|
||||
IFS=','; \
|
||||
for plugin in ${GF_INSTALL_PLUGINS}; do \
|
||||
IFS=$OLDIFS; \
|
||||
if expr match "$plugin" '.*\;.*'; then \
|
||||
pluginUrl=$(echo "$plugin" | cut -d';' -f 1); \
|
||||
pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2); \
|
||||
grafana-cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}"; \
|
||||
else \
|
||||
grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}; \
|
||||
fi \
|
||||
done \
|
||||
fi
|
||||
|
||||
@@ -1,55 +1,50 @@
|
||||
ARG GRAFANA_VERSION="latest"
|
||||
|
||||
FROM grafana/grafana:${GRAFANA_VERSION}-ubuntu
|
||||
|
||||
USER root
|
||||
|
||||
# Set DEBIAN_FRONTEND=noninteractive in environment at build-time
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ARG GF_INSTALL_IMAGE_RENDERER_PLUGIN="false"
|
||||
|
||||
ARG GF_GID="0"
|
||||
ENV GF_PATHS_PLUGINS="/var/lib/grafana-plugins"
|
||||
|
||||
RUN mkdir -p "$GF_PATHS_PLUGINS" && \
|
||||
chown -R grafana:${GF_GID} "$GF_PATHS_PLUGINS"
|
||||
|
||||
RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
apt-get update && \
|
||||
apt-get upgrade -y && \
|
||||
apt-get install -y gdebi-core && \
|
||||
cd /tmp && \
|
||||
curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
|
||||
gdebi --n google-chrome-stable_current_amd64.deb && \
|
||||
apt-get autoremove -y && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
USER grafana
|
||||
|
||||
ENV GF_PLUGIN_RENDERING_CHROME_BIN="/usr/bin/google-chrome"
|
||||
|
||||
RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
grafana-cli \
|
||||
--pluginsDir "$GF_PATHS_PLUGINS" \
|
||||
--pluginUrl https://github.com/grafana/grafana-image-renderer/releases/latest/download/plugin-linux-x64-glibc-no-chromium.zip \
|
||||
plugins install grafana-image-renderer; \
|
||||
fi
|
||||
|
||||
ARG GF_INSTALL_PLUGINS=""
|
||||
|
||||
RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \
|
||||
OLDIFS=$IFS; \
|
||||
IFS=','; \
|
||||
for plugin in ${GF_INSTALL_PLUGINS}; do \
|
||||
IFS=$OLDIFS; \
|
||||
if expr match "$plugin" '.*\;.*'; then \
|
||||
pluginUrl=$(echo "$plugin" | cut -d';' -f 1); \
|
||||
pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2); \
|
||||
grafana-cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}"; \
|
||||
else \
|
||||
grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}; \
|
||||
fi \
|
||||
done \
|
||||
fi
|
||||
ARG GRAFANA_VERSION="latest"
|
||||
|
||||
FROM grafana/grafana:${GRAFANA_VERSION}-ubuntu
|
||||
|
||||
USER root
|
||||
|
||||
ARG GF_INSTALL_IMAGE_RENDERER_PLUGIN="false"
|
||||
|
||||
ARG GF_GID="0"
|
||||
ENV GF_PATHS_PLUGINS="/var/lib/grafana-plugins"
|
||||
|
||||
RUN mkdir -p "$GF_PATHS_PLUGINS" && \
|
||||
chown -R grafana:${GF_GID} "$GF_PATHS_PLUGINS" && \
|
||||
if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
cd /tmp && \
|
||||
curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
|
||||
DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y ./google-chrome-stable_current_amd64.deb && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
rm ./google-chrome-stable_current_amd64.deb \
|
||||
fi
|
||||
|
||||
USER grafana
|
||||
|
||||
ENV GF_PLUGIN_RENDERING_CHROME_BIN="/usr/bin/google-chrome"
|
||||
|
||||
RUN if [ $GF_INSTALL_IMAGE_RENDERER_PLUGIN = "true" ]; then \
|
||||
grafana-cli \
|
||||
--pluginsDir "$GF_PATHS_PLUGINS" \
|
||||
--pluginUrl https://github.com/grafana/grafana-image-renderer/releases/latest/download/plugin-linux-x64-glibc-no-chromium.zip \
|
||||
plugins install grafana-image-renderer; \
|
||||
fi
|
||||
|
||||
ARG GF_INSTALL_PLUGINS=""
|
||||
|
||||
RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \
|
||||
OLDIFS=$IFS; \
|
||||
IFS=','; \
|
||||
for plugin in ${GF_INSTALL_PLUGINS}; do \
|
||||
IFS=$OLDIFS; \
|
||||
if expr match "$plugin" '.*\;.*'; then \
|
||||
pluginUrl=$(echo "$plugin" | cut -d';' -f 1); \
|
||||
pluginInstallFolder=$(echo "$plugin" | cut -d';' -f 2); \
|
||||
grafana-cli --pluginUrl ${pluginUrl} --pluginsDir "${GF_PATHS_PLUGINS}" plugins install "${pluginInstallFolder}"; \
|
||||
else \
|
||||
grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin}; \
|
||||
fi \
|
||||
done \
|
||||
fi
|
||||
|
||||
@@ -24,18 +24,18 @@ _docker_repo=${2:-grafana/grafana}
|
||||
|
||||
# If the tag starts with v, treat this as an official release
|
||||
if echo "$_grafana_tag" | grep -q "^v"; then
|
||||
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
||||
_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
|
||||
else
|
||||
_grafana_version=$_grafana_tag
|
||||
_grafana_version=$_grafana_tag
|
||||
fi
|
||||
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
echo "pushing ${_docker_repo}:${_grafana_version}${TAG_SUFFIX}"
|
||||
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
docker_push_all () {
|
||||
repo=$1
|
||||
tag=$2
|
||||
repo=$1
|
||||
tag=$2
|
||||
|
||||
# Push each image individually
|
||||
docker push "${repo}:${tag}${TAG_SUFFIX}"
|
||||
@@ -52,18 +52,18 @@ docker_push_all () {
|
||||
}
|
||||
|
||||
if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
|
||||
echo "pushing ${_docker_repo}:latest${TAG_SUFFIX}"
|
||||
docker_push_all "${_docker_repo}" "latest"
|
||||
docker_push_all "${_docker_repo}" "${_grafana_version}"
|
||||
# Push to the grafana-dev repository with the expected tag
|
||||
# for running the end to end tests successfully
|
||||
echo "pushing ${_docker_repo}:latest${TAG_SUFFIX}"
|
||||
docker_push_all "${_docker_repo}" "latest"
|
||||
docker_push_all "${_docker_repo}" "${_grafana_version}"
|
||||
# Push to the grafana-dev repository with the expected tag
|
||||
# for running the end to end tests successfully
|
||||
docker push "grafana/grafana-dev:${_grafana_tag}${TAG_SUFFIX}"
|
||||
elif echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -q "beta"; then
|
||||
docker_push_all "${_docker_repo}" "${_grafana_version}"
|
||||
# Push to the grafana-dev repository with the expected tag
|
||||
# for running the end to end tests successfully
|
||||
docker_push_all "${_docker_repo}" "${_grafana_version}"
|
||||
# Push to the grafana-dev repository with the expected tag
|
||||
# for running the end to end tests successfully
|
||||
docker push "grafana/grafana-dev:${_grafana_tag}${TAG_SUFFIX}"
|
||||
elif echo "$_grafana_tag" | grep -q "main"; then
|
||||
docker_push_all "${_docker_repo}" "main"
|
||||
docker_push_all "${_docker_repo}" "main"
|
||||
docker push "grafana/grafana-dev:${_grafana_version}${TAG_SUFFIX}"
|
||||
fi
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
ARG BASE_IMAGE=ubuntu:20.04
|
||||
FROM ${BASE_IMAGE} AS grafana-builder
|
||||
|
||||
ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
|
||||
|
||||
COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
|
||||
|
||||
RUN mkdir /tmp/grafana && tar xzf /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Set DEBIAN_FRONTEND=noninteractive in environment at build-time
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG GF_UID="472"
|
||||
ARG GF_GID="0"
|
||||
|
||||
ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \
|
||||
GF_PATHS_DATA="/var/lib/grafana" \
|
||||
GF_PATHS_HOME="/usr/share/grafana" \
|
||||
GF_PATHS_LOGS="/var/log/grafana" \
|
||||
GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \
|
||||
GF_PATHS_PROVISIONING="/etc/grafana/provisioning"
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
# Install dependencies
|
||||
# We need curl in the image
|
||||
RUN apt-get update && apt-get install -y ca-certificates curl tzdata && \
|
||||
apt-get autoremove -y && rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
COPY --from=grafana-builder /tmp/grafana "$GF_PATHS_HOME"
|
||||
|
||||
RUN if [ ! $(getent group "$GF_GID") ]; then \
|
||||
addgroup --system --gid $GF_GID grafana; \
|
||||
fi
|
||||
|
||||
RUN export GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \
|
||||
mkdir -p "$GF_PATHS_HOME/.aws" && \
|
||||
adduser --system --uid $GF_UID --ingroup "$GF_GID_NAME" grafana && \
|
||||
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
|
||||
"$GF_PATHS_PROVISIONING/dashboards" \
|
||||
"$GF_PATHS_PROVISIONING/notifiers" \
|
||||
"$GF_PATHS_PROVISIONING/plugins" \
|
||||
"$GF_PATHS_PROVISIONING/access-control" \
|
||||
"$GF_PATHS_PROVISIONING/alerting" \
|
||||
"$GF_PATHS_LOGS" \
|
||||
"$GF_PATHS_PLUGINS" \
|
||||
"$GF_PATHS_DATA" && \
|
||||
cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \
|
||||
cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \
|
||||
chown -R "grafana:$GF_GID_NAME" "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING" && \
|
||||
chmod -R 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" "$GF_PATHS_PROVISIONING"
|
||||
|
||||
COPY ./run.sh /run.sh
|
||||
|
||||
USER "$GF_UID"
|
||||
ENTRYPOINT [ "/run.sh" ]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user