Merge remote-tracking branch 'origin/main' into resource-store

This commit is contained in:
Ryan McKinley 2024-06-19 14:57:21 +03:00
commit 0b29ca5eac
50 changed files with 782 additions and 326 deletions

View File

@ -865,7 +865,7 @@ services:
- commands:
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
environment: {}
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
image: grafana/mimir-alpine:r295-a23e559
name: mimir_backend
- environment: {}
image: redis:6.2.11-alpine
@ -1312,7 +1312,7 @@ services:
- commands:
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
environment: {}
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
image: grafana/mimir-alpine:r295-a23e559
name: mimir_backend
- environment: {}
image: redis:6.2.11-alpine
@ -2299,7 +2299,7 @@ services:
- commands:
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
environment: {}
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
image: grafana/mimir-alpine:r295-a23e559
name: mimir_backend
- environment: {}
image: redis:6.2.11-alpine
@ -4154,7 +4154,7 @@ services:
- commands:
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
environment: {}
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
image: grafana/mimir-alpine:r295-a23e559
name: mimir_backend
- environment: {}
image: redis:6.2.11-alpine
@ -4673,7 +4673,7 @@ steps:
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r295-a23e559
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine
@ -4708,7 +4708,7 @@ steps:
- trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack
- trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8
- trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
- trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r295-a23e559
- trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39
- trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32
- trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine

View File

@ -1,5 +1,5 @@
mimir_backend:
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
image: grafana/mimir-alpine:r295-a23e559
container_name: mimir_backend
command:
- -target=backend

View File

@ -151,6 +151,7 @@ Experimental features might be changed or removed without prior notice.
| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins |
| `idForwarding` | Generate signed id token for identity that can be forwarded to plugins and external services |
| `enableNativeHTTPHistogram` | Enables native HTTP Histograms |
| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) |
| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint |
| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards |
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |

View File

@ -423,6 +423,7 @@ github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
github.com/KimMachineGun/automemlimit v0.6.0 h1:p/BXkH+K40Hax+PuWWPQ478hPjsp9h1CPDhLlA3Z37E=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
@ -696,6 +697,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
@ -855,8 +857,6 @@ github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJ
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd h1:hSkbZ9XSyjyBirMeqSqUrK+9HboWrweVlzRNqoBi2d4=
github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4=
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
@ -915,6 +915,7 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ=
github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
@ -1025,6 +1026,7 @@ github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52Cu
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLwFEN7T+Flr8sw=
github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o=
@ -1151,12 +1153,15 @@ github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+v
github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg=
github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo=
github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc=
github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM=
github.com/oapi-codegen/testutil v1.0.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw=
github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk=
@ -1236,6 +1241,7 @@ github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAy
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@ -1258,6 +1264,7 @@ github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
@ -1298,6 +1305,7 @@ github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8=
github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk=
github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@ -1366,6 +1374,7 @@ github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvF
github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=

View File

@ -244,6 +244,7 @@
"@emotion/react": "11.11.4",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@floating-ui/react": "0.26.16",
"@formatjs/intl-durationformat": "^0.2.4",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/aws-sdk": "0.3.3",
"@grafana/azure-sdk": "0.0.3",

View File

@ -112,6 +112,7 @@ export interface FeatureToggles {
externalServiceAccounts?: boolean;
panelMonitoring?: boolean;
enableNativeHTTPHistogram?: boolean;
disableClassicHTTPHistogram?: boolean;
formatString?: boolean;
transformationsVariableSupport?: boolean;
kubernetesPlaylists?: boolean;

View File

@ -603,4 +603,7 @@ export const Components = {
headerOrderSwitch: 'data-testid header-order-switch',
headerPreviewSwitch: 'data-testid header-preview-switch',
},
EntityNotFound: {
container: 'data-testid entity-not-found',
},
};

View File

@ -505,6 +505,20 @@ describe('Language completion provider', () => {
)
);
});
it('should dont send match[] parameter if there is no metric', async () => {
const mockQueries: PromQuery[] = [
{
refId: 'A',
expr: '',
},
];
const fetchLabel = languageProvider.fetchLabels;
const requestSpy = jest.spyOn(languageProvider, 'request');
await fetchLabel(tr, mockQueries);
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy.mock.calls[0][0].indexOf('match[]')).toEqual(-1);
});
});
});

View File

@ -219,11 +219,13 @@ export default class PromQlLanguageProvider extends LanguageProvider {
const searchParams = new URLSearchParams({ ...timeParams });
queries?.forEach((q) => {
const visualQuery = buildVisualQueryFromString(q.expr);
searchParams.append('match[]', visualQuery.query.metric);
if (visualQuery.query.binaryQueries) {
visualQuery.query.binaryQueries.forEach((bq) => {
searchParams.append('match[]', bq.query.metric);
});
if (visualQuery.query.metric !== '') {
searchParams.append('match[]', visualQuery.query.metric);
if (visualQuery.query.binaryQueries) {
visualQuery.query.binaryQueries.forEach((bq) => {
searchParams.append('match[]', bq.query.metric);
});
}
}
});

View File

@ -28,6 +28,10 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] {
params: [{ name: 'Identifier', type: 'string' }],
defaultParams: ['count'],
}),
...createAggregationOperationWithParam(PromOperationId.Quantile, {
params: [{ name: 'Value', type: 'number' }],
defaultParams: [1],
}),
createAggregationOverTime(PromOperationId.SumOverTime),
createAggregationOverTime(PromOperationId.AvgOverTime),
createAggregationOverTime(PromOperationId.MinOverTime),

View File

@ -76,6 +76,10 @@ export const generalTemplates: TemplateData[] = [
template: 'count_values("aaaa",metric_a{})',
description: 'Count number of label values for a label named "aaaa"',
},
{
template: 'quantile by(l) (1,metric_a)',
description: 'Quantile of series in the metric "metric_a" grouped by the label "l"',
},
];
export const counterTemplates: TemplateData[] = [

View File

@ -230,12 +230,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
id: PromOperationId.Pi,
renderer: (model) => `${model.id}()`,
}),
createFunction({
id: PromOperationId.Quantile,
params: [{ name: 'Value', type: 'number' }],
defaultParams: [1],
renderer: functionRendererLeft,
}),
createFunction({ id: PromOperationId.Rad }),
createRangeFunction(PromOperationId.Resets),
createFunction({

View File

@ -51,12 +51,19 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
if features.IsEnabledGlobally(featuremgmt.FlagEnableNativeHTTPHistogram) {
// the recommended default value from the prom_client
// https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L411
// Giving this variable an value means the client will expose the histograms as an
// native histogram instead of normal a normal histogram.
// Giving this variable a value means the client will expose a native
// histogram.
histogramOptions.NativeHistogramBucketFactor = 1.1
// The default value in OTel. It probably good enough for us as well.
histogramOptions.NativeHistogramMaxBucketNumber = 160
histogramOptions.NativeHistogramMinResetDuration = time.Hour
if features.IsEnabledGlobally(featuremgmt.FlagDisableClassicHTTPHistogram) {
// setting Buckets to nil with native options set means the classic
// histogram will no longer be exposed - this can be a good way to
// reduce cardinality in the exposed metrics
histogramOptions.Buckets = nil
}
}
httpRequestDurationHistogram := prometheus.NewHistogramVec(

View File

@ -0,0 +1,86 @@
package zanzana
import (
"context"
"go.uber.org/zap"
"github.com/grafana/grafana/pkg/infra/log"
)
// zanzanaLogger is a grafana logger wrapper compatible with OpenFGA logger interface
type zanzanaLogger struct {
logger *log.ConcreteLogger
}
func newZanzanaLogger() *zanzanaLogger {
logger := log.New("openfga-server")
return &zanzanaLogger{
logger: logger,
}
}
// Simple converter for zap logger fields
func zapFieldsToArgs(fields []zap.Field) []any {
args := make([]any, 0)
for _, f := range fields {
args = append(args, f.Key)
if f.Interface != nil {
args = append(args, f.Interface)
} else if f.String != "" {
args = append(args, f.String)
} else {
args = append(args, f.Integer)
}
}
return args
}
func (l *zanzanaLogger) Debug(msg string, fields ...zap.Field) {
l.logger.Debug(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) Info(msg string, fields ...zap.Field) {
l.logger.Info(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) Warn(msg string, fields ...zap.Field) {
l.logger.Warn(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) Error(msg string, fields ...zap.Field) {
l.logger.Error(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) Panic(msg string, fields ...zap.Field) {
l.logger.Error(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) Fatal(msg string, fields ...zap.Field) {
l.logger.Error(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) DebugWithContext(ctx context.Context, msg string, fields ...zap.Field) {
l.logger.Debug(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) InfoWithContext(ctx context.Context, msg string, fields ...zap.Field) {
l.logger.Info(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) WarnWithContext(ctx context.Context, msg string, fields ...zap.Field) {
l.logger.Warn(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) ErrorWithContext(ctx context.Context, msg string, fields ...zap.Field) {
l.logger.Error(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) PanicWithContext(ctx context.Context, msg string, fields ...zap.Field) {
l.logger.Error(msg, zapFieldsToArgs(fields)...)
}
func (l *zanzanaLogger) FatalWithContext(ctx context.Context, msg string, fields ...zap.Field) {
l.logger.Error(msg, zapFieldsToArgs(fields)...)
}

View File

@ -1,7 +1,6 @@
package zanzana
import (
"github.com/openfga/openfga/pkg/logger"
"github.com/openfga/openfga/pkg/server"
"github.com/openfga/openfga/pkg/storage"
)
@ -10,8 +9,7 @@ func NewServer(store storage.OpenFGADatastore) (*server.Server, error) {
// FIXME(kalleep): add support for more options, configure logging, tracing etc
opts := []server.OpenFGAServiceV1Option{
server.WithDatastore(store),
// FIXME(kalleep): Write and log adapter for open fga logging interface
server.WithLogger(logger.NewNoopLogger()),
server.WithLogger(newZanzanaLogger()),
}
// FIXME(kalleep): Interceptors

View File

@ -700,11 +700,24 @@ var (
FrontendOnly: true,
},
{
Name: "enableNativeHTTPHistogram",
Description: "Enables native HTTP Histograms",
Stage: FeatureStageExperimental,
FrontendOnly: false,
Owner: hostedGrafanaTeam,
Name: "enableNativeHTTPHistogram",
Description: "Enables native HTTP Histograms",
Stage: FeatureStageExperimental,
FrontendOnly: false,
Owner: grafanaBackendServicesSquad,
HideFromAdminPage: true,
AllowSelfServe: false,
RequiresRestart: true,
},
FeatureFlag{
Name: "disableClassicHTTPHistogram",
Description: "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)",
Stage: FeatureStageExperimental,
FrontendOnly: false,
Owner: grafanaBackendServicesSquad,
HideFromAdminPage: true,
AllowSelfServe: false,
RequiresRestart: true,
},
{
Name: "formatString",

View File

@ -92,7 +92,8 @@ pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,tru
idForwarding,experimental,@grafana/identity-access-team,false,false,false
externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false
panelMonitoring,GA,@grafana/dataviz-squad,false,false,true
enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false
enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
formatString,preview,@grafana/dataviz-squad,false,false,true
transformationsVariableSupport,GA,@grafana/dataviz-squad,false,false,true
kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
92 idForwarding experimental @grafana/identity-access-team false false false
93 externalServiceAccounts preview @grafana/identity-access-team false false false
94 panelMonitoring GA @grafana/dataviz-squad false false true
95 enableNativeHTTPHistogram experimental @grafana/hosted-grafana-team @grafana/grafana-backend-services-squad false false true false
96 disableClassicHTTPHistogram experimental @grafana/grafana-backend-services-squad false true false
97 formatString preview @grafana/dataviz-squad false false true
98 transformationsVariableSupport GA @grafana/dataviz-squad false false true
99 kubernetesPlaylists GA @grafana/grafana-app-platform-squad false true false

View File

@ -383,6 +383,10 @@ const (
// Enables native HTTP Histograms
FlagEnableNativeHTTPHistogram = "enableNativeHTTPHistogram"
// FlagDisableClassicHTTPHistogram
// Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)
FlagDisableClassicHTTPHistogram = "disableClassicHTTPHistogram"
// FlagFormatString
// Enable format string transformer
FlagFormatString = "formatString"

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,6 @@ import (
amalert "github.com/prometheus/alertmanager/api/v2/client/alert"
amalertgroup "github.com/prometheus/alertmanager/api/v2/client/alertgroup"
amgeneral "github.com/prometheus/alertmanager/api/v2/client/general"
amreceiver "github.com/prometheus/alertmanager/api/v2/client/receiver"
amsilence "github.com/prometheus/alertmanager/api/v2/client/silence"
"github.com/prometheus/client_golang/prometheus"
@ -487,21 +486,7 @@ func (am *Alertmanager) GetStatus(ctx context.Context) (apimodels.GettableStatus
}
func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) {
params := amreceiver.NewGetReceiversParamsWithContext(ctx)
res, err := am.amClient.Receiver.GetReceivers(params)
if err != nil {
return []apimodels.Receiver{}, err
}
rcvs := make([]apimodels.Receiver, len(res.Payload))
for i, rcv := range res.Payload {
rcvs[i] = apimodels.Receiver{
Name: *rcv.Name,
Integrations: []apimodels.Integration{},
}
}
return rcvs, nil
return am.mimirClient.GetReceivers(ctx)
}
func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*notifier.TestReceiversResult, error) {

View File

@ -717,7 +717,13 @@ func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) {
// We should start with the default config.
rcvs, err := am.GetReceivers(context.Background())
require.NoError(t, err)
require.Equal(t, "empty-receiver", rcvs[0].Name)
require.Equal(t, []apimodels.Receiver{
{
Active: true,
Name: "empty-receiver",
Integrations: []apimodels.Integration{},
},
}, rcvs)
}
func genAlert(active bool, labels map[string]string) amv2.PostableAlert {

View File

@ -11,7 +11,8 @@ import (
)
const (
grafanaAlertmanagerConfigPath = "/api/v1/grafana/config"
grafanaAlertmanagerConfigPath = "/api/v1/grafana/config"
grafanaAlertmanagerReceiversPath = "/api/v1/grafana/receivers"
)
type UserGrafanaConfig struct {
@ -63,3 +64,16 @@ func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimo
func (mc *Mimir) DeleteGrafanaAlertmanagerConfig(ctx context.Context) error {
return mc.doOK(ctx, grafanaAlertmanagerConfigPath, http.MethodDelete, nil)
}
func (mc *Mimir) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) {
response := []apimodels.Receiver{}
// nolint:bodyclose
// closed within `do`
_, err := mc.do(ctx, grafanaAlertmanagerReceiversPath, http.MethodGet, nil, &response)
if err != nil {
return nil, err
}
return response, nil
}

View File

@ -29,6 +29,9 @@ type MimirClient interface {
DeleteGrafanaAlertmanagerConfig(ctx context.Context) error
ShouldPromoteConfig() bool
// Mimir implements an extended version of the receivers API under a different path.
GetReceivers(ctx context.Context) ([]apimodels.Receiver, error)
}
type Mimir struct {

View File

@ -4,10 +4,12 @@ import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/grafana/grafana/pkg/util"
@ -62,6 +64,11 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
if err != nil {
return err
}
url, err := url.Parse(webhook.Url)
if err != nil {
// Should not be possible - NewRequestWithContext should also err if the URL is bad.
return err
}
if webhook.ContentType == "" {
webhook.ContentType = "application/json"
@ -80,7 +87,7 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
resp, err := netClient.Do(request)
if err != nil {
return err
return redactURL(err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
@ -96,16 +103,25 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
if webhook.Validation != nil {
err := webhook.Validation(body, resp.StatusCode)
if err != nil {
ns.log.Debug("Webhook failed validation", "url", webhook.Url, "statuscode", resp.Status, "body", string(body))
ns.log.Debug("Webhook failed validation", "url", url.Redacted(), "statuscode", resp.Status, "body", string(body), "error", err)
return fmt.Errorf("webhook failed validation: %w", err)
}
}
if resp.StatusCode/100 == 2 {
ns.log.Debug("Webhook succeeded", "url", webhook.Url, "statuscode", resp.Status)
ns.log.Debug("Webhook succeeded", "url", url.Redacted(), "statuscode", resp.Status)
return nil
}
ns.log.Debug("Webhook failed", "url", webhook.Url, "statuscode", resp.Status, "body", string(body))
ns.log.Debug("Webhook failed", "url", url.Redacted(), "statuscode", resp.Status, "body", string(body))
return fmt.Errorf("webhook response status %v", resp.Status)
}
func redactURL(err error) error {
var e *url.Error
if !errors.As(err, &e) {
return err
}
e.URL = "<redacted>"
return e
}

View File

@ -25,27 +25,40 @@ func interpolateInterval(flux string, interval time.Duration) string {
var fluxVariableFilterExp = regexp.MustCompile(`(?m)([a-zA-Z]+)\.([a-zA-Z]+)`)
func interpolateFluxSpecificVariables(query queryModel) string {
rawQuery := query.RawQuery
flux := query.RawQuery
matches := fluxVariableFilterExp.FindAllStringSubmatch(flux, -1)
matches := fluxVariableFilterExp.FindAllStringSubmatchIndex(rawQuery, -1)
if matches != nil {
timeRange := query.TimeRange
from := timeRange.From.UTC().Format(time.RFC3339Nano)
to := timeRange.To.UTC().Format(time.RFC3339Nano)
for _, match := range matches {
switch match[2] {
// For query "range(start: v.timeRangeStart, stop: v.timeRangeStop)"
// rawQuery[match[0]:match[1]] will be v.timeRangeStart
// rawQuery[match[2]:match[3]] will be v
// rawQuery[match[4]:match[5]] will be timeRangeStart
fullMatch := rawQuery[match[0]:match[1]]
key := rawQuery[match[4]:match[5]]
switch key {
case "timeRangeStart":
flux = strings.ReplaceAll(flux, match[0], from)
flux = strings.ReplaceAll(flux, fullMatch, from)
case "timeRangeStop":
flux = strings.ReplaceAll(flux, match[0], to)
flux = strings.ReplaceAll(flux, fullMatch, to)
case "windowPeriod":
flux = strings.ReplaceAll(flux, match[0], query.Interval.String())
flux = strings.ReplaceAll(flux, fullMatch, query.Interval.String())
case "bucket":
flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.Bucket+"\"")
// Check if 'bucket' is part of a join query
beforeMatch := rawQuery[:match[0]]
if strings.Contains(beforeMatch, "join.") {
continue
}
flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.Bucket+"\"")
case "defaultBucket":
flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.DefaultBucket+"\"")
flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.DefaultBucket+"\"")
case "organization":
flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.Organization+"\"")
flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.Organization+"\"")
}
}
}

View File

@ -31,6 +31,11 @@ func TestInterpolate(t *testing.T) {
before: `v.timeRangeStart, something.timeRangeStop, XYZ.bucket, uuUUu.defaultBucket, aBcDefG.organization, window.windowPeriod, a91{}.bucket, $__interval, $__interval_ms`,
after: `2021-09-22T10:12:51.310985041Z, 2021-09-22T11:12:51.310985042Z, "grafana2", "grafana3", "grafana1", 1m1.258s, a91{}.bucket, 1m, 61258`,
},
{
name: "don't interpolate bucket variable in join query",
before: `range(start: v.timeRangeStart, stop: v.timeRangeStop) join.left(left: left |> group(), right: right,on:((l,r) => l.bucket == r.id), as: ((l, r) => ({l with name: r.name})))`,
after: `range(start: 2021-09-22T10:12:51.310985041Z, stop: 2021-09-22T11:12:51.310985042Z) join.left(left: left |> group(), right: right,on:((l,r) => l.bucket == r.id), as: ((l, r) => ({l with name: r.name})))`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EmptyState, TextLink, useStyles2 } from '@grafana/ui';
export interface Props {
@ -15,7 +16,7 @@ export function EntityNotFound({ entity = 'Page' }: Props) {
const styles = useStyles2(getStyles);
return (
<div className={styles.container}>
<div className={styles.container} data-testid={selectors.components.EntityNotFound.container}>
<EmptyState message={`${entity} not found`} variant="not-found">
We&apos;re looking but can&apos;t seem to find this {entity.toLowerCase()}. Try returning{' '}
<TextLink href="/">home</TextLink> or seeking help on the{' '}

View File

@ -0,0 +1,20 @@
import '@formatjs/intl-durationformat/polyfill';
import { getI18next } from './index';
export function formatDate(value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string {
if (typeof value === 'string') {
return formatDate(new Date(value), format);
}
const i18n = getI18next();
const dateFormatter = new Intl.DateTimeFormat(i18n.language, format);
return dateFormatter.format(value);
}
export function formatDuration(duration: Intl.DurationInput, options: Intl.DurationFormatOptions = {}) {
const i18n = getI18next();
const dateFormatter = new Intl.DurationFormat(i18n.language, options);
return dateFormatter.format(duration);
}

View File

@ -7,6 +7,7 @@ import { DEFAULT_LANGUAGE, NAMESPACES, VALID_LANGUAGES } from './constants';
import { loadTranslations } from './loadTranslations';
let tFunc: TFunction<string[], undefined> | undefined;
let i18nInstance: typeof i18n;
export async function initializeI18n(language: string): Promise<{ language: string | undefined }> {
// This is a placeholder so we can put a 'comment' in the message json files.
@ -28,7 +29,7 @@ export async function initializeI18n(language: string): Promise<{ language: stri
ns: NAMESPACES,
};
let i18nInstance = i18n;
i18nInstance = i18n;
if (language === 'detect') {
i18nInstance = i18nInstance.use(LanguageDetector);
const detection: DetectorOptions = { order: ['navigator'], caches: [] };
@ -79,10 +80,20 @@ export const t = (id: string, defaultMessage: string, values?: Record<string, un
return tFunc(id, defaultMessage, values);
};
export const i18nDate = (value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string => {
if (typeof value === 'string') {
return i18nDate(new Date(value), format);
export function getI18next() {
if (!tFunc) {
if (process.env.NODE_ENV !== 'test') {
console.warn(
'An attempt to internationalize was made before it was initialized. This was probably caused by calling a locale-aware function in the root module scope, instead of in render'
);
}
if (process.env.NODE_ENV === 'development') {
throw new Error('getI18next was called before i18n was initialized');
}
return i18n;
}
const dateFormatter = new Intl.DateTimeFormat(i18n.language, format);
return dateFormatter.format(value);
};
return i18nInstance || i18n;
}

View File

@ -0,0 +1,13 @@
import {
DurationFormatConstructor,
DurationFormatOptions as _DurationFormatOptions,
DurationInput as _DurationInput,
} from '@formatjs/intl-durationformat/src/types';
declare global {
namespace Intl {
const DurationFormat: DurationFormatConstructor;
type DurationFormatOptions = _DurationFormatOptions;
type DurationInput = _DurationInput;
}
}

View File

@ -32,9 +32,17 @@ export class PerformanceBackend implements EchoBackend<PerformanceEvent, Perform
return;
}
backendSrv.post('/api/frontend-metrics', {
events: this.buffer,
});
backendSrv
.post(
'/api/frontend-metrics',
{
events: this.buffer,
},
{ showErrorAlert: false }
)
.catch(() => {
// Just swallow this error - it's non-critical
});
this.buffer = [];
};

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { i18nDate } from 'app/core/internationalization';
import { formatDate } from 'app/core/internationalization/dates';
import { AccessControlAction, UserSession } from 'app/types';
interface Props {
@ -68,7 +68,7 @@ class BaseUserSessions extends PureComponent<Props, State> {
sessions.map((session, index) => (
<tr key={`${session.id}-${index}`}>
<td>{session.isActive ? 'Now' : session.seenAt}</td>
<td>{i18nDate(session.createdAt, { dateStyle: 'long' })}</td>
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
<td>{session.clientIp}</td>
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
<td>

View File

@ -200,6 +200,20 @@ describe('DashboardScene', () => {
expect(resoredLayout.state.children.map((c) => c.state.key)).toEqual(originalPanelOrder);
});
it('Should exit edit mode and discard panel changes if leaving the dashboard while in panel edit', () => {
const panel = findVizPanelByKey(scene, 'panel-1');
const editPanel = buildPanelEditScene(panel!);
scene.setState({
editPanel,
});
expect(scene.state.editPanel!['_discardChanges']).toBe(false);
scene.exitEditMode({ skipConfirm: true });
expect(scene.state.editPanel!['_discardChanges']).toBe(true);
});
it.each`
prop | value
${'title'} | ${'new title'}

View File

@ -317,6 +317,13 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this.setState({ isEditing: false });
}
// if we are in edit panel, we need to onDiscard()
// so the useEffect cleanup comes later and
// doesn't try to commit the changes
if (this.state.editPanel) {
this.state.editPanel.onDiscard();
}
// Disable grid dragging
this.propagateEditModeChange();
}
@ -385,6 +392,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
public getPageNav(location: H.Location, navIndex: NavIndex) {
const { meta, viewPanelScene, editPanel } = this.state;
if (meta.dashboardNotFound) {
return { text: 'Not found' };
}
let pageNav: NavModelItem = {
text: this.state.title,
url: getDashboardUrl({

View File

@ -0,0 +1,65 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { selectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { configureStore } from 'app/store/configureStore';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
describe('DashboardSceneRenderer', () => {
it('should render Not Found notice when dashboard is not found', async () => {
const scene = transformSaveModelToScene({
meta: {
isSnapshot: true,
dashboardNotFound: true,
canStar: false,
canDelete: false,
canSave: false,
canEdit: false,
canShare: false,
},
dashboard: {
title: 'Not found',
uid: 'uid',
schemaVersion: 0,
// Disabling build in annotations to avoid mocking Grafana data source
annotations: {
list: [
{
builtIn: 1,
datasource: {
type: 'grafana',
uid: '-- Grafana --',
},
enable: false,
hide: true,
iconColor: 'rgba(0, 211, 255, 1)',
name: 'Annotations & Alerts',
type: 'dashboard',
},
],
},
},
});
const store = configureStore({});
const context = getGrafanaContextMock();
render(
<GrafanaContext.Provider value={context}>
<Provider store={store}>
<Router history={locationService.getHistory()}>
<scene.Component model={scene} />
</Router>
</Provider>
</GrafanaContext.Provider>
);
expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
});
});

View File

@ -7,6 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { SceneComponentProps } from '@grafana/scenes';
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
import { getNavModel } from 'app/core/selectors/navModel';
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
import { useSelector } from 'app/types';
@ -35,14 +36,26 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
);
}
const emptyState = <DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} />;
const emptyState = (
<DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} key="dashboard-empty-state" />
);
const withPanels = (
<div className={cx(styles.body, !hasControls && styles.bodyWithoutControls)}>
<div className={cx(styles.body, !hasControls && styles.bodyWithoutControls)} key="dashboard-panels">
<bodyToRender.Component model={bodyToRender} />
</div>
);
const notFound = meta.dashboardNotFound && <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
let body = [withPanels];
if (notFound) {
body = [notFound];
} else if (isEmpty) {
body = [emptyState, withPanels];
}
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
{editPanel && <editPanel.Component model={editPanel} />}
@ -55,7 +68,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
scopes && isScopesExpanded && styles.pageContainerWithScopesExpanded
)}
>
{scopes && <scopes.Component model={scopes} />}
{scopes && !meta.dashboardNotFound && <scopes.Component model={scopes} />}
<NavToolbarActions dashboard={model} />
{controls && hasControls && (
<div
@ -71,10 +84,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
className={styles.scrollbarContainer}
testId={selectors.pages.Dashboard.DashNav.scrollContainer}
>
<div className={cx(styles.canvasContent, isHomePage && styles.homePagePadding)}>
<>{isEmpty && emptyState}</>
{withPanels}
</div>
<div className={cx(styles.canvasContent, isHomePage && styles.homePagePadding)}>{body}</div>
</CustomScrollbar>
</div>
)}

View File

@ -8,6 +8,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { DashboardMeta } from 'app/types';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
@ -163,9 +164,27 @@ describe('NavToolbarActions', () => {
expect(newShareButton).toBeInTheDocument();
});
});
describe('Snapshot', () => {
it('should show link button when is a snapshot', () => {
setup({
isSnapshot: true,
});
expect(screen.queryByTestId('button-snapshot')).toBeInTheDocument();
});
it('should not show link button when is not found dashboard', () => {
setup({
isSnapshot: true,
dashboardNotFound: true,
});
expect(screen.queryByTestId('button-snapshot')).not.toBeInTheDocument();
});
});
});
function setup() {
function setup(meta?: DashboardMeta) {
const dashboard = new DashboardScene({
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
meta: {
@ -177,6 +196,7 @@ function setup() {
canStar: true,
canAdmin: true,
canDelete: true,
...meta,
},
title: 'hello',
uid: 'dash-1',

View File

@ -139,7 +139,7 @@ export function ToolbarActions({ dashboard }: Props) {
toolbarActions.push({
group: 'icon-actions',
condition: meta.isSnapshot && !isEditing,
condition: meta.isSnapshot && !meta.dashboardNotFound && !isEditing,
render: () => (
<GoToSnapshotOriginButton
key="go-to-snapshot-origin"

View File

@ -92,7 +92,9 @@ export class DashboardLoaderSrv {
return result;
})
.catch(() => {
return this._dashboardLoadFailed('Not found', true);
const dash = this._dashboardLoadFailed('Not found', true);
dash.dashboard.uid = uid;
return dash;
});
} else {
throw new Error('Dashboard uid or slug required');

View File

@ -4,7 +4,8 @@ import React, { PureComponent } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Button, Icon, LoadingPlaceholder } from '@grafana/ui';
import { i18nDate, Trans } from 'app/core/internationalization';
import { Trans } from 'app/core/internationalization';
import { formatDate } from 'app/core/internationalization/dates';
import { UserSession } from 'app/types';
interface Props {
@ -50,7 +51,7 @@ class UserSessions extends PureComponent<Props> {
{sessions.map((session: UserSession, index) => (
<tr key={index}>
{session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>}
<td>{i18nDate(session.createdAt, { dateStyle: 'long' })}</td>
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
<td>{session.clientIp}</td>
<td>
{session.browser} on {session.os} {session.osVersion}

View File

@ -15,6 +15,7 @@ import {
durationToMilliseconds,
parseDuration,
TransformationApplicabilityLevels,
TimeRange,
} from '@grafana/data';
import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames';
import { config } from '@grafana/runtime';
@ -292,7 +293,14 @@ export function prepBucketFrames(frames: DataFrame[]): DataFrame[] {
}));
}
export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCalculationOptions): DataFrame {
interface HeatmapCalculationOptionsWithTimeRange extends HeatmapCalculationOptions {
timeRange?: TimeRange;
}
export function calculateHeatmapFromData(
frames: DataFrame[],
options: HeatmapCalculationOptionsWithTimeRange
): DataFrame {
// Find fields in the heatmap
const { xField, yField, xs, ys } = findHeatmapFields(frames);
@ -329,6 +337,9 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa
ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined,
yLog:
scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as 2 | 10 | undefined) : undefined,
xMin: options.timeRange?.from.valueOf(),
xMax: options.timeRange?.to.valueOf(),
});
const frame = {
@ -460,20 +471,23 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
let ySorted = opts?.ySorted ?? false;
// find x and y limits to pre-compute buckets struct
let minX = xSorted ? xs[0] : Infinity;
let minX = opts?.xMin ?? (xSorted ? xs[0] : Infinity);
let minY = ySorted ? ys[0] : Infinity;
let maxX = xSorted ? xs[len - 1] : -Infinity;
let maxX = opts?.xMax ?? (xSorted ? xs[len - 1] : -Infinity);
let maxY = ySorted ? ys[len - 1] : -Infinity;
let yExp = opts?.yLog;
let withPredefX = opts?.xMin != null && opts?.xMax != null;
let withPredefY = opts?.yMin != null && opts?.yMax != null;
for (let i = 0; i < len; i++) {
if (!xSorted) {
if (!xSorted && !withPredefX) {
minX = Math.min(minX, xs[i]);
maxX = Math.max(maxX, xs[i]);
}
if (!ySorted) {
if (!ySorted && !withPredefY) {
if (!yExp || ys[i] > 0) {
minY = Math.min(minY, ys[i]);
maxY = Math.max(maxY, ys[i]);
@ -500,7 +514,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
}
if (xMode === HeatmapCalculationMode.Count) {
// TODO: optionally use view range min/max instead of data range for bucket sizing
let approx = (maxX - minX) / Math.max(xBinIncr - 1, 1);
// nice-ify
let xIncrs = opts?.xTime ? niceTimeIncrs : niceLinearIncrs;
@ -509,7 +522,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
}
if (yMode === HeatmapCalculationMode.Count) {
// TODO: optionally use view range min/max instead of data range for bucket sizing
let approx = (maxY - minY) / Math.max(yBinIncr - 1, 1);
// nice-ify
let yIncrs = opts?.yTime ? niceTimeIncrs : niceLinearIncrs;

View File

@ -78,4 +78,29 @@ describe('toRawSql', () => {
const result = toRawSql(testQuery);
expect(result).toEqual(expected);
});
it('should not wrap * with quote', () => {
const expected = 'SELECT * FROM "TestValue" WHERE "time" >= $__timeFrom AND "time" <= $__timeTo LIMIT 50';
const testQuery: SQLQuery = {
refId: 'A',
sql: {
limit: 50,
columns: [
{
parameters: [
{
name: '*',
type: QueryEditorExpressionType.FunctionParameter,
},
],
type: QueryEditorExpressionType.Function,
},
],
},
dataset: 'iox',
table: 'TestValue',
};
const result = toRawSql(testQuery);
expect(result).toEqual(expected);
});
});

View File

@ -26,7 +26,10 @@ export function toRawSql({ sql, table }: SQLQuery): string {
}
// wrapping the column name with quotes
const sc = sql.columns.map((c) => ({ ...c, parameters: c.parameters?.map((p) => ({ ...p, name: `"${p.name}"` })) }));
const sc = sql.columns.map((c) => ({
...c,
parameters: c.parameters?.map((p) => ({ ...p, name: formatTableName(p.name) })),
}));
rawQuery += createSelectClause(sc);
if (table) {
@ -66,6 +69,16 @@ export function toRawSql({ sql, table }: SQLQuery): string {
return rawQuery;
}
// When the column name is *, do not wrap the column name in double-quotes.
// See: https://github.com/grafana/grafana/issues/88008
function formatTableName(parameter: string | undefined): string {
if (parameter === '*') {
return parameter;
}
return `"${parameter}"`;
}
const isLimit = (limit: number | undefined): boolean => limit !== undefined && limit >= 0;
// Puts double quotes (") around the identifier if it is necessary.

View File

@ -40,7 +40,7 @@ export class InfluxVariableSupport extends CustomVariableSupport<InfluxDatasourc
query: interpolated,
maxDataPoints: request.targets[0].maxDataPoints ?? 1000,
},
request.range
{ range: request.range }
)
);
return metricFindStream.pipe(map((results) => ({ data: results })));

View File

@ -59,11 +59,19 @@ export const HeatmapPanel = ({
const info = useMemo(() => {
try {
return prepareHeatmapData(data.series, data.annotations, options, palette, theme, replaceVariables);
return prepareHeatmapData({
frames: data.series,
annotations: data.annotations,
options,
palette,
theme,
replaceVariables,
timeRange,
});
} catch (ex) {
return { warning: `${ex}` };
}
}, [data.series, data.annotations, options, palette, theme, replaceVariables]);
}, [data.series, data.annotations, options, palette, theme, replaceVariables, timeRange]);
const facets = useMemo(() => {
let exemplarsXFacet: number[] | undefined = []; // "Time" field

View File

@ -10,6 +10,7 @@ import {
GrafanaTheme2,
InterpolateFunction,
outerJoinDataFrames,
TimeRange,
ValueFormatter,
} from '@grafana/data';
import { parseSampleValue, sortSeriesByLabel } from '@grafana/prometheus';
@ -65,14 +66,25 @@ export interface HeatmapData {
warning?: string;
}
export function prepareHeatmapData(
frames: DataFrame[],
annotations: DataFrame[] | undefined,
options: Options,
palette: string[],
theme: GrafanaTheme2,
replaceVariables: InterpolateFunction = (v) => v
): HeatmapData {
interface PrepareHeatmapDataOptions {
frames: DataFrame[];
annotations?: DataFrame[];
options: Options;
palette: string[];
theme: GrafanaTheme2;
replaceVariables?: InterpolateFunction;
timeRange?: TimeRange;
}
export function prepareHeatmapData({
frames,
annotations,
options,
palette,
theme,
replaceVariables = (v) => v,
timeRange,
}: PrepareHeatmapDataOptions): HeatmapData {
if (!frames?.length) {
return {};
}
@ -104,7 +116,7 @@ export function prepareHeatmapData(
}
return getDenseHeatmapData(
calculateHeatmapFromData(frames, optionsCopy.calculation ?? {}),
calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
exemplars,
optionsCopy,
palette,
@ -113,7 +125,7 @@ export function prepareHeatmapData(
}
return getDenseHeatmapData(
calculateHeatmapFromData(frames, options.calculation ?? {}),
calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
exemplars,
options,
palette,

View File

@ -53,7 +53,12 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
// NOTE: this feels like overkill/expensive just to assert if we have an ordinal y
// can probably simplify without doing full dataprep
const palette = quantizeScheme(opts.color, config.theme2);
const v = prepareHeatmapData(context.data, undefined, opts, palette, config.theme2);
const v = prepareHeatmapData({
frames: context.data,
options: opts,
palette,
theme: config.theme2,
});
isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null;
} catch {}
}

View File

@ -20,7 +20,12 @@ export class HeatmapSuggestionsSupplier {
}
const palette = quantizeScheme(defaultOptions.color, config.theme2);
const info = prepareHeatmapData(builder.data.series, undefined, defaultOptions, palette, config.theme2);
const info = prepareHeatmapData({
frames: builder.data.series,
options: defaultOptions,
palette,
theme: config.theme2,
});
if (!info || info.warning) {
return;
}

View File

@ -21,7 +21,7 @@ images = {
"plugins_slack": "plugins/slack",
"python": "python:3.8",
"postgres_alpine": "postgres:12.3-alpine",
"mimir": "us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP",
"mimir": "grafana/mimir-alpine:r295-a23e559",
"mysql5": "mysql:5.7.39",
"mysql8": "mysql:8.0.32",
"redis_alpine": "redis:6.2.11-alpine",

View File

@ -2503,6 +2503,16 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:2.0.0":
version: 2.0.0
resolution: "@formatjs/ecma402-abstract@npm:2.0.0"
dependencies:
"@formatjs/intl-localematcher": "npm:0.5.4"
tslib: "npm:^2.4.0"
checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234
languageName: node
linkType: hard
"@formatjs/fast-memoize@npm:1.2.6":
version: 1.2.6
resolution: "@formatjs/fast-memoize@npm:1.2.6"
@ -2533,6 +2543,17 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-durationformat@npm:^0.2.4":
version: 0.2.4
resolution: "@formatjs/intl-durationformat@npm:0.2.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.0.0"
"@formatjs/intl-localematcher": "npm:0.5.4"
tslib: "npm:^2.4.0"
checksum: 10/5f500409a20d18967e17ffbc222f9b4c4bf7ef08cce20023c33f06d1989c2bc4cf700d1dd1d048748d0a36c882109d5375896a4964d6700f73ec18914c6de4ba
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.2.31":
version: 0.2.31
resolution: "@formatjs/intl-localematcher@npm:0.2.31"
@ -2542,6 +2563,15 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.5.4":
version: 0.5.4
resolution: "@formatjs/intl-localematcher@npm:0.5.4"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10/780cb29b42e1ea87f2eb5db268577fcdc53da52d9f096871f3a1bb78603b4ba81d208ea0b0b9bc21548797c941ce435321f62d2522795b83b740f90b0ceb5778
languageName: node
linkType: hard
"@gar/promisify@npm:^1.0.1":
version: 1.1.3
resolution: "@gar/promisify@npm:1.1.3"
@ -17036,6 +17066,7 @@ __metadata:
"@emotion/react": "npm:11.11.4"
"@fingerprintjs/fingerprintjs": "npm:^3.4.2"
"@floating-ui/react": "npm:0.26.16"
"@formatjs/intl-durationformat": "npm:^0.2.4"
"@glideapps/glide-data-grid": "npm:^6.0.0"
"@grafana/aws-sdk": "npm:0.3.3"
"@grafana/azure-sdk": "npm:0.0.3"