From a01b4c31a1fe358420095e0278b10437e8425e8a Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 10 Jul 2025 18:47:18 -0500 Subject: [PATCH 01/25] Transformations: Allow queries without dataframe results to be selectable as filters (#107152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add mention of external type to correlations docs * 🆗 * Merge request targets in with results * Ensure we do not mutate result set --- .../TransformationOperationRow.tsx | 12 +++++++++++- .../TransformationsEditor/TransformationsEditor.tsx | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx index b77e0f41620..bebae5de3e2 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx @@ -138,7 +138,17 @@ export const TransformationOperationRow = ({ .subscribe(setOutput); const prevOutputSubscription = transformDataFrame(prevInputTransforms, data.series, ctx) .pipe(mergeMap((before) => transformDataFrame(prevOutputTransforms, before, ctx))) - .subscribe(setPrevOutput); + .subscribe((result) => { + let mergedResult = [...result]; + // add refIds that were requested even if they did not return a result + data.request?.targets.forEach((series) => { + const refIdInResult = mergedResult.some((frame) => frame.refId === series.refId); + if (!refIdInResult) { + mergedResult.push({ refId: series.refId, fields: [], length: 0 }); + } + }); + setPrevOutput(mergedResult); + }); return function unsubscribe() { inputSubscription.unsubscribe(); diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index 17d18efba74..afdf6b782ea 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -5,6 +5,7 @@ import { Unsubscribable } from 'rxjs'; import { DataFrame, + DataQueryRequest, DataTransformerConfig, PanelData, SelectableValue, @@ -44,6 +45,7 @@ export type viewAllType = 'viewAll'; export type FilterCategory = TransformerCategory | viewAllType; export interface TransformationData { + request?: DataQueryRequest; series: DataFrame[]; annotations?: DataFrame[]; } From b1c50d63549c456586606ba61732e004112007a3 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Fri, 11 Jul 2025 01:48:50 -0400 Subject: [PATCH 02/25] Revert "KBARResults: Add Cmd+Enter/Ctrl+Enter to open links in new tab" (#107933) Revert "KBARResults: Add Cmd+Enter/Ctrl+Enter to open links in new tab (#107537)" This reverts commit a2a28b207cb3af11361819b6c717ff6ec2a66945. --- public/app/features/commandPalette/KBarResults.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/public/app/features/commandPalette/KBarResults.tsx b/public/app/features/commandPalette/KBarResults.tsx index ae7604b6350..2eb8a5aecf7 100644 --- a/public/app/features/commandPalette/KBarResults.tsx +++ b/public/app/features/commandPalette/KBarResults.tsx @@ -71,18 +71,7 @@ export const KBarResults = (props: KBarResultsProps) => { } return nextIndex; }); - } else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { - // Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux) - open in new tab - event.preventDefault(); - - if (activeRef.current instanceof HTMLAnchorElement) { - window.open(activeRef.current.href, '_blank', 'noopener,noreferrer'); - query.toggle(); - } else { - // For action-based items (rendered as
tags), execute normally - activeRef.current?.click(); - } - } else if (event.key === 'Enter' && !event.metaKey && !event.ctrlKey) { + } else if (event.key === 'Enter' && !event.metaKey) { event.preventDefault(); // storing the active dom element in a ref prevents us from // having to calculate the current action to perform based From dcea36dffcd35e68509bf96e9b0e15ca24e37bf7 Mon Sep 17 00:00:00 2001 From: Robert Horvath Date: Fri, 11 Jul 2025 08:44:47 +0200 Subject: [PATCH 03/25] Add Relyance github action (#107012) * add relyance workflow * add relyance config * exclude folders and patterns from scanning * add security as codeowner of relyance --- .github/CODEOWNERS | 4 ++ .github/workflows/relyance-scan.yml | 33 ++++++++++ relyance.yaml | 99 +++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 .github/workflows/relyance-scan.yml create mode 100644 relyance.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 13c155ac1af..ec932f79bf3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -876,3 +876,7 @@ embed.go @grafana/grafana-as-code /conf/provisioning/datasources/ @grafana/plugins-platform-backend /conf/provisioning/plugins/ @grafana/plugins-platform-backend /conf/provisioning/sample/ @grafana/grafana-git-ui-sync-team + +# Security +/relyance.yaml @grafana/security-team +/.github/workflows/relyance-scan.yml @grafana/security-team diff --git a/.github/workflows/relyance-scan.yml b/.github/workflows/relyance-scan.yml new file mode 100644 index 00000000000..29d68e00416 --- /dev/null +++ b/.github/workflows/relyance-scan.yml @@ -0,0 +1,33 @@ +name: Relyance Compliance Inspection +on: + schedule: + - cron: '0 0 * * *' # Run daily at 00:00 UTC + workflow_dispatch: # Allow for manual trigger + +jobs: + relyance-compliance-inspector: + permissions: + contents: read + id-token: write # Needed for Vault access + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Get API key + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@5d7e361bc7e0a183cde8afe9899fb7b596d2659b # get-vault-secrets-v1.2.0 + with: + repo_secrets: | + API_KEY=relyance:API_KEY + + - name: Run Relyance scan + env: + API_KEY: "${{ fromJSON(steps.vault-secrets.outputs.secrets).API_KEY }}" + run: | + docker pull gcr.io/relyance-ext/compliance_inspector:release && \ + docker run --rm -v ${{ github.workspace }}:/repo --env "API_KEY=${{ env.API_KEY }}" gcr.io/relyance-ext/compliance_inspector:release diff --git a/relyance.yaml b/relyance.yaml new file mode 100644 index 00000000000..41a798d27f2 --- /dev/null +++ b/relyance.yaml @@ -0,0 +1,99 @@ +# Relyance Compliance Inspector Configuration + +repository: grafana/grafana + +# Services and Code Relationship +# Purpose: Configuration used to visualize the code base and relationships between microservices and code. +# Note: Directory paths are relative to the repository's root directory. +# Note: Need a separate entry for each microservice. See example below. +services: + + + +# Example: Services and Code Relationship +# Note: Do not use below. It is an example. +# Note: Directory paths are relative to the repository's root directory +#- service: paymentservice +# source: +# - src/payments +# - src/checkout_service +#- service: catalogservice +# source: +# - src/catalog + + +# Source Directories to Exclude +# Purpose: Configuration is used to exclude directories from analysis such as test directories etc. +# Note: Directory paths are relative to the repository's root directory +exclude: + # Test and development environments + - e2e + - devenv + - hack + - scripts + - tools + - pkg/tests + + # Documentation and examples + - docs + - contribute + - grafana-mixin + + # Build artifacts and dependencies + - packaging + - data + - bin + - plugins-bundled + - vendor + - node_modules + + # Static UI assets + - public/fonts + - public/img + - public/locales + - public/vendor + - public/lib + + # Configuration templates + - conf + - emails + - cue.mod + - kinds + - kindsv2 + + # Temporary and cache directories + - tmp + - cache + - .cache + - venv + + +# Excluding files and directories using patterns (enable lines as needed) +exclude_patterns: + # Test and mock files + - "**/*_test.go" + - "**/*.test.ts" + - "**/*.spec.ts" + - "**/testdata/**" + - "**/*_mock.go" + - "**/mocks/**" + + # Generated code + - "**/*_gen.go" + - "**/wire_gen.go" + + # Documentation and examples + - "**/*.md" + - "**/README.*" + - "**/LICENSE*" + - "**/*.example.*" + + # System files and dependencies + - "**/.DS_Store" + - "**/*.log" + - "**/go.sum" + - "**/yarn.lock" + + # Generated specs + - "**/swagger.json" + - "**/openapi*.json" From ac86126a57d5096d334f02bf8d23537bd0dad0de Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 11 Jul 2025 00:49:41 -0700 Subject: [PATCH 04/25] Chore: Update auto-generated clients (#107986) --- .../clients/advisor/v0alpha1/endpoints.gen.ts | 329 +----------------- .../preferences/api/user/endpoints.gen.ts | 12 +- 2 files changed, 14 insertions(+), 327 deletions(-) diff --git a/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts b/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts index 86ca7fa2a38..d054b90a9f4 100644 --- a/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/advisor/v0alpha1/endpoints.gen.ts @@ -1,15 +1,11 @@ import { api } from './baseAPI'; -export const addTagTypes = ['API Discovery', 'Check', 'CheckType'] as const; +export const addTagTypes = ['Check', 'CheckType'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, }) .injectEndpoints({ endpoints: (build) => ({ - getApiResources: build.query({ - query: () => ({ url: `/apis/advisor.grafana.app/v0alpha1/` }), - providesTags: ['API Discovery'], - }), listCheck: build.query({ query: (queryArg) => ({ url: `/checks`, @@ -43,29 +39,6 @@ const injectedRtkApi = api }), invalidatesTags: ['Check'], }), - deletecollectionCheck: build.mutation({ - query: (queryArg) => ({ - url: `/checks`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - continue: queryArg['continue'], - dryRun: queryArg.dryRun, - fieldSelector: queryArg.fieldSelector, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - labelSelector: queryArg.labelSelector, - limit: queryArg.limit, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - resourceVersion: queryArg.resourceVersion, - resourceVersionMatch: queryArg.resourceVersionMatch, - sendInitialEvents: queryArg.sendInitialEvents, - timeoutSeconds: queryArg.timeoutSeconds, - }, - }), - invalidatesTags: ['Check'], - }), getCheck: build.query({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -75,20 +48,6 @@ const injectedRtkApi = api }), providesTags: ['Check'], }), - replaceCheck: build.mutation({ - query: (queryArg) => ({ - url: `/checks/${queryArg.name}`, - method: 'PUT', - body: queryArg.check, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['Check'], - }), deleteCheck: build.mutation({ query: (queryArg) => ({ url: `/checks/${queryArg.name}`, @@ -138,81 +97,6 @@ const injectedRtkApi = api }), providesTags: ['CheckType'], }), - createCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes`, - method: 'POST', - body: queryArg.checkType, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['CheckType'], - }), - deletecollectionCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - continue: queryArg['continue'], - dryRun: queryArg.dryRun, - fieldSelector: queryArg.fieldSelector, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - labelSelector: queryArg.labelSelector, - limit: queryArg.limit, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - resourceVersion: queryArg.resourceVersion, - resourceVersionMatch: queryArg.resourceVersionMatch, - sendInitialEvents: queryArg.sendInitialEvents, - timeoutSeconds: queryArg.timeoutSeconds, - }, - }), - invalidatesTags: ['CheckType'], - }), - getCheckType: build.query({ - query: (queryArg) => ({ - url: `/checktypes/${queryArg.name}`, - params: { - pretty: queryArg.pretty, - }, - }), - providesTags: ['CheckType'], - }), - replaceCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes/${queryArg.name}`, - method: 'PUT', - body: queryArg.checkType, - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - fieldManager: queryArg.fieldManager, - fieldValidation: queryArg.fieldValidation, - }, - }), - invalidatesTags: ['CheckType'], - }), - deleteCheckType: build.mutation({ - query: (queryArg) => ({ - url: `/checktypes/${queryArg.name}`, - method: 'DELETE', - params: { - pretty: queryArg.pretty, - dryRun: queryArg.dryRun, - gracePeriodSeconds: queryArg.gracePeriodSeconds, - ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, - orphanDependents: queryArg.orphanDependents, - propagationPolicy: queryArg.propagationPolicy, - }, - }), - invalidatesTags: ['CheckType'], - }), updateCheckType: build.mutation({ query: (queryArg) => ({ url: `/checktypes/${queryArg.name}`, @@ -232,8 +116,6 @@ const injectedRtkApi = api overrideExisting: false, }); export { injectedRtkApi as generatedAPI }; -export type GetApiResourcesApiResponse = /** status 200 OK */ ApiResourceList; -export type GetApiResourcesApiArg = void; export type ListCheckApiResponse = /** status 200 OK */ CheckList; export type ListCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ @@ -294,57 +176,6 @@ export type CreateCheckApiArg = { fieldValidation?: string; check: Check; }; -export type DeletecollectionCheckApiResponse = /** status 200 OK */ Status; -export type DeletecollectionCheckApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - - This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ - continue?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ - fieldSelector?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ - labelSelector?: string; - /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - - The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ - limit?: number; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; - /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersion?: string; - /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersionMatch?: string; - /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - - When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan - is interpreted as "data at least as new as the provided `resourceVersion`" - and the bookmark event is send when the state is synced - to a `resourceVersion` at least as fresh as the one provided by the ListOptions. - If `resourceVersion` is unset, this is interpreted as "consistent read" and the - bookmark event is send when the state is synced at least to the moment - when request started being processed. - - `resourceVersionMatch` set to any other value or unset - Invalid error is returned. - - Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ - sendInitialEvents?: boolean; - /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ - timeoutSeconds?: number; -}; export type GetCheckApiResponse = /** status 200 OK */ Check; export type GetCheckApiArg = { /** name of the Check */ @@ -352,20 +183,6 @@ export type GetCheckApiArg = { /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ pretty?: string; }; -export type ReplaceCheckApiResponse = /** status 200 OK */ Check | /** status 201 Created */ Check; -export type ReplaceCheckApiArg = { - /** name of the Check */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - check: Check; -}; export type DeleteCheckApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; export type DeleteCheckApiArg = { /** name of the Check */ @@ -444,110 +261,6 @@ export type ListCheckTypeApiArg = { /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ watch?: boolean; }; -export type CreateCheckTypeApiResponse = /** status 200 OK */ - | CheckType - | /** status 201 Created */ CheckType - | /** status 202 Accepted */ CheckType; -export type CreateCheckTypeApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - checkType: CheckType; -}; -export type DeletecollectionCheckTypeApiResponse = /** status 200 OK */ Status; -export type DeletecollectionCheckTypeApiArg = { - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". - - This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ - continue?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ - fieldSelector?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ - labelSelector?: string; - /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. - - The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ - limit?: number; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; - /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersion?: string; - /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. - - Defaults to unset */ - resourceVersionMatch?: string; - /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. - - When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan - is interpreted as "data at least as new as the provided `resourceVersion`" - and the bookmark event is send when the state is synced - to a `resourceVersion` at least as fresh as the one provided by the ListOptions. - If `resourceVersion` is unset, this is interpreted as "consistent read" and the - bookmark event is send when the state is synced at least to the moment - when request started being processed. - - `resourceVersionMatch` set to any other value or unset - Invalid error is returned. - - Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ - sendInitialEvents?: boolean; - /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ - timeoutSeconds?: number; -}; -export type GetCheckTypeApiResponse = /** status 200 OK */ CheckType; -export type GetCheckTypeApiArg = { - /** name of the CheckType */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; -}; -export type ReplaceCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; -export type ReplaceCheckTypeApiArg = { - /** name of the CheckType */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. */ - fieldManager?: string; - /** fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered. */ - fieldValidation?: string; - checkType: CheckType; -}; -export type DeleteCheckTypeApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; -export type DeleteCheckTypeApiArg = { - /** name of the CheckType */ - name: string; - /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ - pretty?: string; - /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ - dryRun?: string; - /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ - gracePeriodSeconds?: number; - /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ - ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; - /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ - orphanDependents?: boolean; - /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ - propagationPolicy?: string; -}; export type UpdateCheckTypeApiResponse = /** status 200 OK */ CheckType | /** status 201 Created */ CheckType; export type UpdateCheckTypeApiArg = { /** name of the CheckType */ @@ -564,38 +277,6 @@ export type UpdateCheckTypeApiArg = { force?: boolean; patch: Patch; }; -export type ApiResource = { - /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ - categories?: string[]; - /** group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale". */ - group?: string; - /** kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo') */ - kind: string; - /** name is the plural name of the resource. */ - name: string; - /** namespaced indicates if a resource is namespaced or not. */ - namespaced: boolean; - /** shortNames is a list of suggested short names of the resource. */ - shortNames?: string[]; - /** singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface. */ - singularName: string; - /** The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates. */ - storageVersionHash?: string; - /** verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy) */ - verbs: string[]; - /** version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)". */ - version?: string; -}; -export type ApiResourceList = { - /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** groupVersion is the group and version this APIResourceList is for. */ - groupVersion: string; - /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - /** resources contains the name of the resources and if they are namespaced. */ - resources: ApiResource[]; -}; export type Time = string; export type FieldsV1 = object; export type ManagedFieldsEntry = { @@ -709,14 +390,14 @@ export type CheckReportFailure = { itemID: string; /** Links to actions that can be taken to resolve the failure */ links: CheckErrorLink[]; - /** More information about the failure */ + /** More information about the failure, not meant to be displayed to the user. Used for LLM suggestions. */ moreInfo?: string; /** Severity of the failure */ severity: string; /** Step ID that the failure is associated with */ stepID: string; }; -export type CheckV0Alpha1StatusReport = { +export type CheckReport = { /** Number of elements analyzed */ count: number; /** List of failures */ @@ -731,7 +412,7 @@ export type CheckStatus = { operatorStates?: { [key: string]: CheckstatusOperatorState; }; - report: CheckV0Alpha1StatusReport; + report: CheckReport; }; export type Check = { /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ @@ -739,6 +420,7 @@ export type Check = { /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; metadata: ObjectMeta; + /** Spec is the spec of the Check */ spec: CheckSpec; status: CheckStatus; }; @@ -843,6 +525,7 @@ export type CheckType = { /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ kind?: string; metadata: ObjectMeta; + /** Spec is the spec of the CheckType */ spec: CheckTypeSpec; status: CheckTypeStatus; }; diff --git a/public/app/features/preferences/api/user/endpoints.gen.ts b/public/app/features/preferences/api/user/endpoints.gen.ts index 1ad485b471c..39b3ecf7560 100644 --- a/public/app/features/preferences/api/user/endpoints.gen.ts +++ b/public/app/features/preferences/api/user/endpoints.gen.ts @@ -44,10 +44,10 @@ export type Preferences = { homeDashboardUID?: string; /** Selected language (beta) */ language?: string; - /** Selected locale (beta) */ - locale?: string; navbar?: NavbarPreference; queryHistory?: QueryHistoryPreference; + /** Selected locale (beta) */ + regionalFormat?: string; /** light, dark, empty is default */ theme?: string; /** The timezone selection @@ -72,22 +72,26 @@ export type SuccessResponseBody = { export type CookieType = string; export type PatchPrefsCmd = { cookies?: CookieType[]; + /** The numerical :id of a favorited dashboard */ + homeDashboardId?: number; homeDashboardUID?: string; language?: string; - locale?: string; navbar?: NavbarPreference; queryHistory?: QueryHistoryPreference; + regionalFormat?: string; theme?: 'light' | 'dark'; timezone?: 'utc' | 'browser'; weekStart?: string; }; export type UpdatePrefsCmd = { cookies?: CookieType[]; + /** The numerical :id of a favorited dashboard */ + homeDashboardId?: number; homeDashboardUID?: string; language?: string; - locale?: string; navbar?: NavbarPreference; queryHistory?: QueryHistoryPreference; + regionalFormat?: string; theme?: 'light' | 'dark' | 'system'; timezone?: 'utc' | 'browser'; weekStart?: string; From a314b995896cc1ae1b53b40e610c0ef3002c431c Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 11 Jul 2025 10:10:30 +0200 Subject: [PATCH 05/25] Remote Alertmanager: Use the same struct for Grafana state and Mimir full state (#107791) Remote Alertmanager: Use the same struct for Grafana stat and Mimir full state --- .../notifier/multiorg_alertmanager_remote_test.go | 6 +++--- pkg/services/ngalert/remote/client/alertmanager_state.go | 9 +++++---- pkg/services/ngalert/remote/client/mimir.go | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go index 2a378f622d9..2d60c284535 100644 --- a/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go +++ b/pkg/services/ngalert/notifier/multiorg_alertmanager_remote_test.go @@ -108,7 +108,7 @@ func TestMultiorgAlertmanager_RemoteSecondaryMode(t *testing.T) { // It should send config and state on startup. var lastConfig *remoteClient.UserGrafanaConfig - var lastState *remoteClient.UserGrafanaState + var lastState *remoteClient.UserState { // We should start with no config and no state in the external Alertmanager. require.Empty(t, fakeAM.config) @@ -174,7 +174,7 @@ func newFakeRemoteAlertmanager(t *testing.T, user, pass string) *fakeRemoteAlert type fakeRemoteAlertmanager struct { t *testing.T config *remoteClient.UserGrafanaConfig - state *remoteClient.UserGrafanaState + state *remoteClient.UserState username string password string } @@ -240,7 +240,7 @@ func (f *fakeRemoteAlertmanager) getConfig(w http.ResponseWriter) { } func (f *fakeRemoteAlertmanager) postState(w http.ResponseWriter, r *http.Request) { - var state remoteClient.UserGrafanaState + var state remoteClient.UserState require.NoError(f.t, json.NewDecoder(r.Body).Decode(&state)) f.state = &state diff --git a/pkg/services/ngalert/remote/client/alertmanager_state.go b/pkg/services/ngalert/remote/client/alertmanager_state.go index cac59c01192..39bf2968572 100644 --- a/pkg/services/ngalert/remote/client/alertmanager_state.go +++ b/pkg/services/ngalert/remote/client/alertmanager_state.go @@ -12,12 +12,13 @@ const ( grafanaAlertmanagerStatePath = "/api/v1/grafana/state" ) -type UserGrafanaState struct { +// UserState is used for both Grafana state and Mimir full state. +type UserState struct { State string `json:"state"` } -func (mc *Mimir) GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaState, error) { - gs := &UserGrafanaState{} +func (mc *Mimir) GetGrafanaAlertmanagerState(ctx context.Context) (*UserState, error) { + gs := &UserState{} response := successResponse{ Data: gs, } @@ -36,7 +37,7 @@ func (mc *Mimir) GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaS } func (mc *Mimir) CreateGrafanaAlertmanagerState(ctx context.Context, state string) error { - payload, err := json.Marshal(&UserGrafanaState{ + payload, err := json.Marshal(&UserState{ State: state, }) if err != nil { diff --git a/pkg/services/ngalert/remote/client/mimir.go b/pkg/services/ngalert/remote/client/mimir.go index 61075dc1df4..5bc03102e3f 100644 --- a/pkg/services/ngalert/remote/client/mimir.go +++ b/pkg/services/ngalert/remote/client/mimir.go @@ -24,7 +24,7 @@ import ( // MimirClient contains all the methods to query the migration critical endpoints of Mimir instance, it's an interface to allow multiple implementations. type MimirClient interface { - GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaState, error) + GetGrafanaAlertmanagerState(ctx context.Context) (*UserState, error) CreateGrafanaAlertmanagerState(ctx context.Context, state string) error DeleteGrafanaAlertmanagerState(ctx context.Context) error From ea0ddb3fc956ee5102f35ba34844482bcb81db02 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Fri, 11 Jul 2025 11:10:19 +0200 Subject: [PATCH 06/25] unistore: refactor get to return a reader (#107951) --- pkg/storage/unified/resource/datastore.go | 6 +-- pkg/storage/unified/resource/eventstore.go | 13 ++++--- pkg/storage/unified/resource/kv.go | 30 +++++--------- pkg/storage/unified/resource/kv_test.go | 12 +++--- pkg/storage/unified/resource/metadata.go | 6 +-- pkg/storage/unified/resource/metadata_test.go | 7 ++-- pkg/storage/unified/testing/kv.go | 39 +++++++++---------- 7 files changed, 48 insertions(+), 65 deletions(-) diff --git a/pkg/storage/unified/resource/datastore.go b/pkg/storage/unified/resource/datastore.go index 836008117ba..5838b585b8f 100644 --- a/pkg/storage/unified/resource/datastore.go +++ b/pkg/storage/unified/resource/datastore.go @@ -239,11 +239,7 @@ func (d *dataStore) Get(ctx context.Context, key DataKey) (io.ReadCloser, error) return nil, fmt.Errorf("invalid data key: %w", err) } - obj, err := d.kv.Get(ctx, dataSection, key.String()) - if err != nil { - return nil, err - } - return obj.Value, nil + return d.kv.Get(ctx, dataSection, key.String()) } func (d *dataStore) Save(ctx context.Context, key DataKey, value io.Reader) error { diff --git a/pkg/storage/unified/resource/eventstore.go b/pkg/storage/unified/resource/eventstore.go index 6070d1a6eeb..e1f4b565952 100644 --- a/pkg/storage/unified/resource/eventstore.go +++ b/pkg/storage/unified/resource/eventstore.go @@ -147,16 +147,15 @@ func (n *eventStore) Get(ctx context.Context, key EventKey) (Event, error) { return Event{}, fmt.Errorf("invalid event key: %w", err) } - obj, err := n.kv.Get(ctx, eventsSection, key.String()) + reader, err := n.kv.Get(ctx, eventsSection, key.String()) if err != nil { return Event{}, err } + defer func() { _ = reader.Close() }() var event Event - if err = json.NewDecoder(obj.Value).Decode(&event); err != nil { - _ = obj.Value.Close() + if err = json.NewDecoder(reader).Decode(&event); err != nil { return Event{}, err } - defer func() { _ = obj.Value.Close() }() return event, nil } @@ -173,14 +172,16 @@ func (n *eventStore) ListSince(ctx context.Context, sinceRV int64) iter.Seq2[Eve if err != nil { return } - obj, err := n.kv.Get(ctx, eventsSection, key) + reader, err := n.kv.Get(ctx, eventsSection, key) if err != nil { return } var event Event - if err := json.NewDecoder(obj.Value).Decode(&event); err != nil { + if err := json.NewDecoder(reader).Decode(&event); err != nil { + _ = reader.Close() return } + _ = reader.Close() if !yield(event, nil) { return } diff --git a/pkg/storage/unified/resource/kv.go b/pkg/storage/unified/resource/kv.go index 21343984c8c..0e9c7a13d0e 100644 --- a/pkg/storage/unified/resource/kv.go +++ b/pkg/storage/unified/resource/kv.go @@ -29,18 +29,12 @@ type ListOptions struct { Limit int64 // maximum number of results to return. 0 means no limit. } -// KVObject represents a key-value object -type KVObject struct { - Key string // the key of the object within the section - Value io.ReadCloser // the value of the object -} - type KV interface { // Keys returns all the keys in the store Keys(ctx context.Context, section string, opt ListOptions) iter.Seq2[string, error] - // Get retrieves a key-value pair from the store - Get(ctx context.Context, section string, key string) (KVObject, error) + // Get retrieves the value for a key from the store + Get(ctx context.Context, section string, key string) (io.ReadCloser, error) // Save a new value Save(ctx context.Context, section string, key string, value io.Reader) error @@ -67,16 +61,16 @@ func NewBadgerKV(db *badger.DB) *badgerKV { } } -func (k *badgerKV) Get(ctx context.Context, section string, key string) (KVObject, error) { +func (k *badgerKV) Get(ctx context.Context, section string, key string) (io.ReadCloser, error) { if k.db.IsClosed() { - return KVObject{}, fmt.Errorf("database is closed") + return nil, fmt.Errorf("database is closed") } txn := k.db.NewTransaction(false) defer txn.Discard() if section == "" { - return KVObject{}, fmt.Errorf("section is required") + return nil, fmt.Errorf("section is required") } key = section + "/" + key @@ -84,24 +78,18 @@ func (k *badgerKV) Get(ctx context.Context, section string, key string) (KVObjec item, err := txn.Get([]byte(key)) if err != nil { if errors.Is(err, badger.ErrKeyNotFound) { - return KVObject{}, ErrNotFound + return nil, ErrNotFound } - return KVObject{}, err - } - - out := KVObject{ - Key: string(item.Key())[len(section)+1:], + return nil, err } // Get the value and create a reader from it value, err := item.ValueCopy(nil) if err != nil { - return KVObject{}, err + return nil, err } - out.Value = io.NopCloser(bytes.NewReader(value)) - - return out, nil + return io.NopCloser(bytes.NewReader(value)), nil } func (k *badgerKV) Save(ctx context.Context, section string, key string, value io.Reader) error { diff --git a/pkg/storage/unified/resource/kv_test.go b/pkg/storage/unified/resource/kv_test.go index a2e3544db7c..6b7ead40879 100644 --- a/pkg/storage/unified/resource/kv_test.go +++ b/pkg/storage/unified/resource/kv_test.go @@ -116,20 +116,20 @@ func TestBadgerKV_UnderlyingStorage(t *testing.T) { require.NoError(t, err) // Verify KV interface returns correct values for each section - obj1, err := kv.Get(ctx, section1, key) + reader1, err := kv.Get(ctx, section1, key) require.NoError(t, err) - val1, err := io.ReadAll(obj1.Value) + val1, err := io.ReadAll(reader1) require.NoError(t, err) require.Equal(t, value1, string(val1)) - err = obj1.Value.Close() + err = reader1.Close() require.NoError(t, err) - obj2, err := kv.Get(ctx, section2, key) + reader2, err := kv.Get(ctx, section2, key) require.NoError(t, err) - val2, err := io.ReadAll(obj2.Value) + val2, err := io.ReadAll(reader2) require.NoError(t, err) require.Equal(t, value2, string(val2)) - err = obj2.Value.Close() + err = reader2.Close() require.NoError(t, err) }) diff --git a/pkg/storage/unified/resource/metadata.go b/pkg/storage/unified/resource/metadata.go index 0ed7d4ec493..ba0ea429faf 100644 --- a/pkg/storage/unified/resource/metadata.go +++ b/pkg/storage/unified/resource/metadata.go @@ -206,15 +206,15 @@ func (d *metadataStore) Get(ctx context.Context, key MetaDataKey) (MetaData, err return MetaData{}, fmt.Errorf("invalid metadata key: %w", err) } - obj, err := d.kv.Get(ctx, metaSection, key.String()) + reader, err := d.kv.Get(ctx, metaSection, key.String()) if err != nil { return MetaData{}, err } defer func() { - _ = obj.Value.Close() + _ = reader.Close() }() var meta MetaData - err = json.NewDecoder(obj.Value).Decode(&meta) + err = json.NewDecoder(reader).Decode(&meta) return meta, err } diff --git a/pkg/storage/unified/resource/metadata_test.go b/pkg/storage/unified/resource/metadata_test.go index 70c3ea26a32..2e4816a3c32 100644 --- a/pkg/storage/unified/resource/metadata_test.go +++ b/pkg/storage/unified/resource/metadata_test.go @@ -110,11 +110,12 @@ func TestMetadataStore_Save(t *testing.T) { }) require.NoError(t, err) // Verify in the kv store that the metadata is saved - retrievedObj, err := store.kv.Get(ctx, metaSection, key.String()) + reader, err := store.kv.Get(ctx, metaSection, key.String()) require.NoError(t, err) - assert.Equal(t, key.String(), retrievedObj.Key) var retrivedMeta MetaData - actualData, err := io.ReadAll(retrievedObj.Value) + actualData, err := io.ReadAll(reader) + require.NoError(t, err) + err = reader.Close() require.NoError(t, err) err = json.Unmarshal(actualData, &retrivedMeta) require.NoError(t, err) diff --git a/pkg/storage/unified/testing/kv.go b/pkg/storage/unified/testing/kv.go index 0c3b745fdf0..60b69d38497 100644 --- a/pkg/storage/unified/testing/kv.go +++ b/pkg/storage/unified/testing/kv.go @@ -85,17 +85,16 @@ func runTestKVGet(t *testing.T, kv resource.KV, nsPrefix string) { require.NoError(t, err) // Now get it - obj, err := kv.Get(ctx, section, "existing-key") + reader, err := kv.Get(ctx, section, "existing-key") require.NoError(t, err) - assert.Equal(t, "existing-key", obj.Key) // Read the value - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, testValue, string(value)) // Close the value reader - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) @@ -122,14 +121,13 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { require.NoError(t, err) // Verify it was saved - obj, err := kv.Get(ctx, section, "new-key") + reader, err := kv.Get(ctx, section, "new-key") require.NoError(t, err) - assert.Equal(t, "new-key", obj.Key) - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, testValue, string(value)) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) @@ -144,13 +142,13 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { require.NoError(t, err) // Verify it was updated - obj, err := kv.Get(ctx, section, "overwrite-key") + reader, err := kv.Get(ctx, section, "overwrite-key") require.NoError(t, err) - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, newValue, string(value)) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) @@ -166,13 +164,13 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { require.NoError(t, err) // Verify binary data - obj, err := kv.Get(ctx, section, "binary-key") + reader, err := kv.Get(ctx, section, "binary-key") require.NoError(t, err) - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, binaryData, value) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) @@ -182,15 +180,14 @@ func runTestKVSave(t *testing.T, kv resource.KV, nsPrefix string) { require.NoError(t, err) // Verify it was saved with empty data - obj, err := kv.Get(ctx, section, "empty-key") + reader, err := kv.Get(ctx, section, "empty-key") require.NoError(t, err) - assert.Equal(t, "empty-key", obj.Key) - value, err := io.ReadAll(obj.Value) + value, err := io.ReadAll(reader) require.NoError(t, err) assert.Equal(t, "", string(value)) assert.Len(t, value, 0) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) }) } @@ -416,14 +413,14 @@ func runTestKVConcurrent(t *testing.T, kv resource.KV, nsPrefix string) { } // Get immediately - obj, err := kv.Get(ctx, section, key) + reader, err := kv.Get(ctx, section, key) if err != nil { return } - readValue, err := io.ReadAll(obj.Value) + readValue, err := io.ReadAll(reader) require.NoError(t, err) - err = obj.Value.Close() + err = reader.Close() require.NoError(t, err) assert.Equal(t, value, string(readValue)) } From b6580ccb103942d3ea26c52fac320810aa93fa1e Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 11 Jul 2025 10:31:33 +0100 Subject: [PATCH 07/25] E2E: migrate to playwright (#107241) * separate playwright into its own folder * better separation * add login scenario, add tags * remove ui option * update CODEOWNERS * add a panels suite test * migrate queries test to playwright * rename + add dashlist test * add panelEdit_base * add geomap map controls test * add geomap-layer-types * add geomap-spatial-operations tests * add frontend-sandbox-panel tests * add smoke-tests-suite * add comment about adding datasource * add dashboard-browse-nested * add dashboard-browse * add dashboard-export-json * add dashboard-keybindings test * remove @wip tag * turn on screenshots and add comment for why this test fails * add dashboard-links-without-slug test * try adding permissions in the test as well * add dashboard-live-streaming * context in the test doesn't work - sad * create dashboard-public-templating * add dashboard-public-create and make live streaming more resilient * add share externally test * add dashboard-share-internally * add share-snapshot-create test * add dashboard-templating * add timepicker tests * add embedded-dashboard test * add general_dashboards test * add import-dashboard test * add load-options-from-url test * add new-constant-variable test * add custom-variable test * add new-datasource-variable test * add new-interval-variable test * add text-box-variable test * add new-query-variable test * add horizontal repeat test * add panel-vertical-repeat test * add empty-row-repeat test * add set-options-from-ui test * add snapshot-create test * add templating test * add textbox-variables test * add cloud-plugins-suite * add storybook verification tests * add playwright storybook verification workflow * add playwright browsers * update CODEOWNERS * test change to trigger storybook verification workflows * try container instead * get the version right... * go back to installing - less chance of forgetting to update * Basic Github Actions Squashed commit of the following: commit f84c650a71bba339da7f96303292717569d4caa6 Author: joshhunt Date: Tue Jul 1 13:23:46 2025 +0100 add arg for sharding, but not using it yet commit 7bcf0512c6652e079757e7f892d1802eb255a6a7 Author: joshhunt Date: Tue Jul 1 12:30:30 2025 +0100 less newline commit b6439118829a09a4c8c49d7692d38df0dff7e7ae Author: joshhunt Date: Tue Jul 1 12:24:31 2025 +0100 less logs commit 38f871e9c235344f12ef31121fdfced549c5323c Author: joshhunt Date: Tue Jul 1 10:00:26 2025 +0100 fix yaml commit db9a773136eff40a82beb3b2f55f73ab0d07a717 Author: joshhunt Date: Tue Jul 1 09:57:47 2025 +0100 clean up files commit c0525f41fa8b498f2fdf71b5047301b0e2945498 Author: joshhunt Date: Tue Jul 1 09:44:56 2025 +0100 gha workflow commit 895bea7c527bfbb148a421b9283fa0805f6ab11d Author: joshhunt Date: Mon Jun 30 19:33:08 2025 +0100 working dagger commit cea1f8443738e9e4b375c065dd4a1e466036bb74 Author: joshhunt Date: Mon Jun 30 16:17:46 2025 +0100 wip * shard gha * some tidy up * add flags for exporting results, and a gha step to merge runs * fix shard gha * add dashboard-duplicate-panel test * add dashboard-outline test * add dashboards-add-panel * remove some commented out code * add dashboards-title-description test * add dashboards-remove-panel * don't install cypress * gha: check playwright results * add dashboards-edit-adhoc-variables test * fix check-jobs * add dagger cloud token * add dagger cloud token * add edit-datasource-variable test * update CODEOWNERS * add dashboards-edit-group-by-variables (skipped for now) * add dashboards-edit-panel-title-description test * add dashboards-edit-transparent-bg test * add dashboards-edit-query-variables test * run with 8 shards * add dashboards-edit-variables * tidy up gha * add dashboard-group-panels * fix action * try to cache the grafana build * fix missing action becuase no checkout, use builtin continue-on-error instead * fix missing id * cat out.txt * debug build cache * fix debug build cache * add dashboards-panel-layouts test * tidy up * no more debug * fix grafana dir * add dashboards-move-panel test * skip some failing tests * mark up plugins tests with @plugins tag, only run @plugins tests in drone * Hackathon/Playwright Conversion - Various Suite (#107516) * Playwright Migration: Various Suite tests * skipping bad tests * fix some tests that can fail * fix uid * separate user for the verify-i18n test * build test plugins for grafana server * properly blur input fields * login manually * get dashboardPage from goto * ignore a couple of type assertions * remove a couple of timeouts * remove timeouts on dashboard-share-internally * use toBeHidden * make dashboard-share-internally more stable * remove TEMP_DAGGER_TOKEN * clean up visaulization-suggestions * unskip gauge test * unskip trace-view-scrolling * attempt to make create variable utils stable * unskip loki tests * make go linter happy * unskip edit-group-by-variables test * unskip move panel tests * isolate dashboard-timepicker tests with separate user * create data source as part of smoke test * make sure we're awaiting in dashboard-edit-adhoc-variables * make dashboards-edit-variables test more robust * Hackathon Playwright: Dashboards Search (#107580) * Hackathon Playwright: Dashboards Search * Feedback changes * make trace-view-scrolling more stable * add json report and bench step * fix bench version * move fail step to after the playwright report so we can report test failures * fix output file name * fix typo * try wrap in expect.poll * stability * bit more tidy up * fix dashboard-new-layouts tests * move test-plugins to e2e-playwright * fix go code for drone e2e run * move loki plugin-e2e test * make v2 dashboards work again --------- Co-authored-by: joshhunt Co-authored-by: Josh Hunt Co-authored-by: Collin Fingar Co-authored-by: Jeff Levin --- .betterer.eslint.config.js | 2 +- .drone.yml | 6 +- .github/CODEOWNERS | 9 +- .github/actions/check-jobs/action.yml | 48 + .github/workflows/pr-e2e-tests.yml | 191 +- .github/workflows/release-pr.yml | 2 +- .../workflows/run-dashboard-search-e2e.yml | 4 +- .../storybook-verification-playwright.yml | 45 + .gitignore | 2 +- contribute/style-guides/e2e-plugins.md | 4 +- .../cloud-plugins-suite/azure-monitor.spec.ts | 544 ++ .../dashboard-duplicate-panel.spec.ts | 56 + .../dashboard-group-panels.spec.ts | 804 ++ .../dashboard-outline.spec.ts | 38 + .../dashboards-add-panel.spec.ts | 44 + .../dashboards-edit-adhoc-variables.spec.ts | 91 + ...shboards-edit-datasource-variables.spec.ts | 62 + ...dashboards-edit-group-by-variables.spec.ts | 89 + ...oards-edit-panel-title-description.spec.ts | 58 + ...shboards-edit-panel-transparent-bg.spec.ts | 43 + .../dashboards-edit-query-variables.spec.ts | 81 + .../dashboards-edit-variables.spec.ts | 183 + .../dashboards-move-panel.spec.ts | 120 + .../dashboards-panel-layouts.spec.ts | 409 + .../dashboards-remove-panel.spec.ts | 66 + .../dashboards-title-description.spec.ts | 47 + e2e-playwright/dashboard-new-layouts/utils.ts | 84 + .../dashboards-search.spec.ts | 63 + .../dashboard-browse-nested.spec.ts | 126 + .../dashboards-suite/dashboard-browse.spec.ts | 102 + .../dashboard-export-json.spec.ts | 66 + .../dashboard-keybindings.spec.ts | 60 + .../dashboard-links-without-slug.spec.ts | 65 + .../dashboard-live-streaming.spec.ts | 48 + .../dashboard-public-create.spec.ts | 211 + .../dashboard-public-templating.spec.ts | 56 + .../dashboard-share-externally-create.spec.ts | 191 + .../dashboard-share-internally.spec.ts | 175 + .../dashboard-share-snapshot-create.spec.ts | 69 + .../dashboard-templating.spec.ts | 74 + .../dashboard-timepicker.spec.ts | 97 + .../embedded-dashboard.spec.ts | 26 + .../general-dashboards.spec.ts | 47 + .../dashboards-suite/import-dashboard.spec.ts | 63 + .../load-options-from-url.spec.ts | 300 + .../new-constant-variable.spec.ts | 65 + .../new-custom-variable.spec.ts | 121 + .../new-datasource-variable.spec.ts | 74 + .../new-interval-variable.spec.ts | 84 + .../new-query-variable.spec.ts | 251 + .../new-text-box-variable.spec.ts | 56 + .../repeating-a-panel-horizontally.spec.ts | 132 + .../repeating-a-panel-vertically.spec.ts | 132 + .../repeating-an-empty-row.spec.ts | 114 + .../set-options-from-ui.spec.ts | 280 + .../dashboards-suite/snapshot-create.spec.ts | 69 + ...ting-dashboard-links-and-variables.spec.ts | 64 + .../textbox-variables.spec.ts | 56 + .../dashboards-suite/utils/makeDashboard.ts | 58 + .../dashboards/DashboardLiveTest.json | 107 + .../dashboards/DataLinkWithoutSlugTest.json | 255 + .../dashboards/PanelSandboxDashboard.json | 57 + e2e-playwright/dashboards/TestDashboard.json | 208 + .../dashboards/TestV2Dashboard.json | 424 + .../fixtures/exemplars-query-response.json | 323 + .../fixtures/long-trace-response.json | 7592 +++++++++++++++++ e2e-playwright/fixtures/tempo-response.json | 1181 +++ e2e-playwright/panels-suite/dashlist.spec.ts | 56 + .../frontend-sandbox-panel.spec.ts | 154 + .../panels-suite/geomap-layer-types.spec.ts | 137 + .../panels-suite/geomap-map-controls.spec.ts | 83 + ...eomap-spatial-operations-transform.spec.ts | 194 + .../panels-suite/panelEdit_base.spec.ts | 109 + .../panels-suite/panelEdit_queries.spec.ts | 118 + .../panels-suite/panelEdit_transforms.spec.ts | 44 + .../azuremonitor/azuremonitor.spec.ts | 14 + .../cloudmonitoring/cloudmonitoring.spec.ts | 14 + .../plugin-e2e/cloudwatch/cloudwatch.spec.ts | 14 + .../elasticsearch/elasticsearch.spec.ts | 14 + .../plugin-e2e/graphite/graphite.spec.ts | 14 + .../plugin-e2e/influxdb/influxdb.spec.ts | 14 + .../plugin-e2e/jaeger/jaeger.spec.ts | 14 + .../plugin-e2e/loki/loki.spec.ts | 4 +- e2e-playwright/plugin-e2e/mssql/mssql.spec.ts | 14 + .../plugin-e2e/mysql/mocks/mysql.mocks.ts | 0 e2e-playwright/plugin-e2e/mysql/mysql.spec.ts | 31 + .../plugin-e2e/mysql/utils.ts | 0 .../mysql/visual-query-builder.spec.ts | 176 + .../plugin-e2e/opentsdb/opentsdb.spec.ts | 14 + .../plugin-e2e/plugin-e2e-api-tests/README.md | 0 .../as-admin-user/alerting.spec.ts | 31 + .../as-admin-user/annotationEditPage.spec.ts | 22 +- .../as-admin-user/dashboard.spec.ts | 23 + .../datasourceConfigPage.spec.ts | 48 + .../as-admin-user/explorePage.spec.ts | 19 + .../as-admin-user/featureToggles.spec.ts | 25 + .../as-admin-user/lokiEditor.spec.ts | 95 + .../as-admin-user/panelDataAssertion.spec.ts | 105 + .../as-admin-user/panelEditPage.spec.ts | 214 + .../as-admin-user/variableEditPage.spec.ts | 25 + .../as-viewer-user/permissions.spec.ts | 24 + .../plugin-e2e/plugin-e2e-api-tests/errors.ts | 0 .../plugin-e2e-api-tests/mocks/queries.ts | 0 .../plugin-e2e-api-tests/mocks/resources.ts | 0 .../plugin-e2e/postgres/postgres.spec.ts | 14 + .../plugin-e2e/zipkin/zipkin.spec.ts | 14 + e2e-playwright/scenarios/login.spec.ts | 19 + .../smoke-tests-suite/1-smoketests.spec.ts | 54 + .../panels-smokescreen.spec.ts | 49 + .../start-and-run-suite | 0 e2e-playwright/storybook/verify.spec.ts | 18 + .../test-plugins/README.md | 0 .../frontend-sandbox-app-test/img/logo.svg | 0 .../frontend-sandbox-app-test/module.js | 0 .../frontend-sandbox-app-test/plugin.json | 0 .../img/logo.svg | 0 .../module.js | 0 .../plugin.json | 0 .../frontend-sandbox-panel-test/img/logo.svg | 0 .../frontend-sandbox-panel-test/module.js | 0 .../frontend-sandbox-panel-test/plugin.json | 0 .../grafana-extensionstest-app/.gitignore | 0 .../grafana-extensionstest-app/CHANGELOG.md | 0 .../grafana-extensionstest-app/README.md | 0 .../components/ActionButton/ActionButton.tsx | 0 .../components/ActionButton/index.ts | 0 .../components/App/App.tsx | 0 .../components/App/index.tsx | 0 .../components/QueryModal/QueryModal.tsx | 0 .../components/QueryModal/index.ts | 0 .../grafana-extensionstest-app/constants.ts | 0 .../grafana-extensionstest-app/img/logo.svg | 0 .../grafana-extensionstest-app/module.tsx | 0 .../grafana-extensionstest-app/package.json | 0 .../pages/AddedComponents.tsx | 0 .../pages/AddedLinks.tsx | 0 .../pages/ExposedComponents.tsx | 0 .../pages/index.tsx | 0 .../grafana-extensionstest-app/plugin.json | 0 .../components/App/App.tsx | 0 .../components/App/index.tsx | 0 .../img/logo.svg | 0 .../grafana-extensionexample1-app/module.tsx | 0 .../grafana-extensionexample1-app/plugin.json | 0 .../components/App/App.tsx | 0 .../components/App/index.tsx | 0 .../img/logo.svg | 0 .../grafana-extensionexample2-app/module.tsx | 0 .../grafana-extensionexample2-app/plugin.json | 0 .../components/App/AddedLinks.tsx | 0 .../components/App/App.tsx | 0 .../img/logo.svg | 0 .../grafana-extensionexample3-app/module.tsx | 0 .../grafana-extensionexample3-app/plugin.json | 0 .../grafana-extensionstest-app/testIds.ts | 0 .../tests/useExposedComponent.spec.ts | 16 + .../tests/usePluginComponents.spec.ts | 19 + .../tests/usePluginLinks.spec.ts | 76 + .../grafana-extensionstest-app/tests/utils.ts | 0 .../grafana-extensionstest-app/tsconfig.json | 0 .../utils/utils.routing.ts | 0 .../grafana-extensionstest-app/utils/utils.ts | 0 .../webpack.config.ts | 0 .../grafana-test-datasource/CHANGELOG.md | 0 .../grafana-test-datasource/README.md | 0 .../components/ConfigEditor.tsx | 0 .../components/QueryEditor.tsx | 0 .../grafana-test-datasource/datasource.ts | 0 .../grafana-test-datasource/img/logo.svg | 0 .../grafana-test-datasource/module.ts | 0 .../grafana-test-datasource/package.json | 0 .../grafana-test-datasource/plugin.json | 0 .../tests/configEditor.spec.ts | 47 + .../tests/variables.spec.ts | 21 + .../grafana-test-datasource/tsconfig.json | 0 .../grafana-test-datasource/types.ts | 0 .../grafana-test-datasource/variables.ts | 0 .../grafana-test-datasource/webpack.config.ts | 0 e2e-playwright/utils/dashboard-helpers.ts | 35 + e2e-playwright/utils/prometheus-helpers.ts | 52 + .../various-suite/bar-gauge.spec.ts | 20 + .../various-suite/bookmarks.spec.ts | 91 + .../various-suite/exemplars.spec.ts | 150 + e2e-playwright/various-suite/explore.spec.ts | 29 + .../various-suite/filter-annotations.spec.ts | 121 + .../frontend-sandbox-app.spec.ts | 77 + .../frontend-sandbox-datasource.spec.ts | 203 + e2e-playwright/various-suite/gauge.spec.ts | 27 + .../various-suite/graph-auto-migrate.spec.ts | 71 + .../various-suite/inspect-drawer.spec.ts | 173 + e2e-playwright/various-suite/keybinds.spec.ts | 89 + .../various-suite/loki-query-builder.spec.ts | 112 + .../loki-table-explore-to-dash.spec.ts | 204 + .../various-suite/migrate-to-cloud.spec.ts | 393 + .../various-suite/navigation.spec.ts | 79 + .../various-suite/pie-chart.spec.ts | 21 + .../prometheus-annotations.spec.ts | 137 + .../various-suite/prometheus-config.spec.ts | 193 + .../various-suite/prometheus-editor.spec.ts | 287 + .../prometheus-variable-editor.spec.ts | 171 + .../various-suite/query-editor.spec.ts | 52 + .../various-suite/return-to-previous.spec.ts | 133 + .../various-suite/solo-route.spec.ts | 67 + .../trace-view-scrolling.spec.ts | 63 + .../various-suite/verify-i18n.spec.ts | 79 + .../visualization-suggestions.spec.ts | 47 + .../azuremonitor/azuremonitor.spec.ts | 8 - .../cloudmonitoring/cloudmonitoring.spec.ts | 8 - e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts | 8 - .../elasticsearch/elasticsearch.spec.ts | 8 - e2e/plugin-e2e/graphite/graphite.spec.ts | 8 - e2e/plugin-e2e/influxdb/influxdb.spec.ts | 8 - e2e/plugin-e2e/jaeger/jaeger.spec.ts | 8 - e2e/plugin-e2e/mssql/mssql.spec.ts | 8 - e2e/plugin-e2e/mysql/mysql.spec.ts | 25 - .../mysql/visual-query-builder.spec.ts | 164 - e2e/plugin-e2e/opentsdb/opentsdb.spec.ts | 8 - .../as-admin-user/alerting.spec.ts | 28 - .../as-admin-user/dashboard.spec.ts | 15 - .../datasourceConfigPage.spec.ts | 37 - .../as-admin-user/explorePage.spec.ts | 8 - .../as-admin-user/featureToggles.spec.ts | 17 - .../as-admin-user/lokiEditor.spec.ts | 87 - .../as-admin-user/panelDataAssertion.spec.ts | 101 - .../as-admin-user/panelEditPage.spec.ts | 206 - .../as-admin-user/variableEditPage.spec.ts | 17 - .../as-viewer-user/permissions.spec.ts | 16 - e2e/plugin-e2e/postgres/postgres.spec.ts | 8 - e2e/plugin-e2e/zipkin/zipkin.spec.ts | 8 - .../tests/useExposedComponent.spec.ts | 8 - .../tests/usePluginComponents.spec.ts | 11 - .../tests/usePluginLinks.spec.ts | 71 - .../tests/configEditor.spec.ts | 39 - .../tests/variables.spec.ts | 13 - eslint.config.js | 2 +- package.json | 5 +- pkg/build/a11y/main.go | 2 +- pkg/build/a11y/service.go | 2 +- pkg/build/e2e-playwright/README.md | 22 + pkg/build/e2e-playwright/e2e.go | 101 + pkg/build/e2e-playwright/frontend.go | 84 + pkg/build/e2e-playwright/main.go | 243 + pkg/build/e2e-playwright/service.go | 58 + pkg/build/e2e/service.go | 1 + playwright.config.ts | 102 +- playwright.storybook.config.ts | 35 + scripts/drone/steps/lib.star | 2 +- scripts/grafana-server/custom.ini | 5 + scripts/grafana-server/start-server | 32 +- yarn.lock | 8 +- 250 files changed, 22677 insertions(+), 1038 deletions(-) create mode 100644 .github/actions/check-jobs/action.yml create mode 100644 .github/workflows/storybook-verification-playwright.yml create mode 100644 e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts create mode 100644 e2e-playwright/dashboard-new-layouts/utils.ts create mode 100644 e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-browse.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-templating.spec.ts create mode 100644 e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts create mode 100644 e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts create mode 100644 e2e-playwright/dashboards-suite/general-dashboards.spec.ts create mode 100644 e2e-playwright/dashboards-suite/import-dashboard.spec.ts create mode 100644 e2e-playwright/dashboards-suite/load-options-from-url.spec.ts create mode 100644 e2e-playwright/dashboards-suite/new-constant-variable.spec.ts create mode 100644 e2e-playwright/dashboards-suite/new-custom-variable.spec.ts create mode 100644 e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts create mode 100644 e2e-playwright/dashboards-suite/new-interval-variable.spec.ts create mode 100644 e2e-playwright/dashboards-suite/new-query-variable.spec.ts create mode 100644 e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts create mode 100644 e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts create mode 100644 e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts create mode 100644 e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts create mode 100644 e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts create mode 100644 e2e-playwright/dashboards-suite/snapshot-create.spec.ts create mode 100644 e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts create mode 100644 e2e-playwright/dashboards-suite/textbox-variables.spec.ts create mode 100644 e2e-playwright/dashboards-suite/utils/makeDashboard.ts create mode 100644 e2e-playwright/dashboards/DashboardLiveTest.json create mode 100644 e2e-playwright/dashboards/DataLinkWithoutSlugTest.json create mode 100644 e2e-playwright/dashboards/PanelSandboxDashboard.json create mode 100644 e2e-playwright/dashboards/TestDashboard.json create mode 100644 e2e-playwright/dashboards/TestV2Dashboard.json create mode 100644 e2e-playwright/fixtures/exemplars-query-response.json create mode 100644 e2e-playwright/fixtures/long-trace-response.json create mode 100644 e2e-playwright/fixtures/tempo-response.json create mode 100644 e2e-playwright/panels-suite/dashlist.spec.ts create mode 100644 e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts create mode 100644 e2e-playwright/panels-suite/geomap-layer-types.spec.ts create mode 100644 e2e-playwright/panels-suite/geomap-map-controls.spec.ts create mode 100644 e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts create mode 100644 e2e-playwright/panels-suite/panelEdit_base.spec.ts create mode 100644 e2e-playwright/panels-suite/panelEdit_queries.spec.ts create mode 100644 e2e-playwright/panels-suite/panelEdit_transforms.spec.ts create mode 100644 e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts create mode 100644 e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts create mode 100644 e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts create mode 100644 e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts create mode 100644 e2e-playwright/plugin-e2e/graphite/graphite.spec.ts create mode 100644 e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts create mode 100644 e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts rename {e2e => e2e-playwright}/plugin-e2e/loki/loki.spec.ts (53%) create mode 100644 e2e-playwright/plugin-e2e/mssql/mssql.spec.ts rename {e2e => e2e-playwright}/plugin-e2e/mysql/mocks/mysql.mocks.ts (100%) create mode 100644 e2e-playwright/plugin-e2e/mysql/mysql.spec.ts rename {e2e => e2e-playwright}/plugin-e2e/mysql/utils.ts (100%) create mode 100644 e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts create mode 100644 e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts rename {e2e => e2e-playwright}/plugin-e2e/plugin-e2e-api-tests/README.md (100%) create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts rename {e2e => e2e-playwright}/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts (62%) create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts create mode 100644 e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts rename {e2e => e2e-playwright}/plugin-e2e/plugin-e2e-api-tests/errors.ts (100%) rename {e2e => e2e-playwright}/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts (100%) rename {e2e => e2e-playwright}/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts (100%) create mode 100644 e2e-playwright/plugin-e2e/postgres/postgres.spec.ts create mode 100644 e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts create mode 100644 e2e-playwright/scenarios/login.spec.ts create mode 100644 e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts create mode 100644 e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts rename {e2e/plugin-e2e => e2e-playwright}/start-and-run-suite (100%) create mode 100644 e2e-playwright/storybook/verify.spec.ts rename {e2e => e2e-playwright}/test-plugins/README.md (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-app-test/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-app-test/module.js (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-app-test/plugin.json (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-datasource-test/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-datasource-test/module.js (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-datasource-test/plugin.json (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-panel-test/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-panel-test/module.js (100%) rename {e2e => e2e-playwright}/test-plugins/frontend-sandbox-panel-test/plugin.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/.gitignore (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/CHANGELOG.md (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/README.md (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/components/App/App.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/components/App/index.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/constants.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/module.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/package.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/pages/index.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugin.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/testIds.ts (100%) create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/tests/utils.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/tsconfig.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/utils/utils.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-extensionstest-app/webpack.config.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/CHANGELOG.md (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/README.md (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/components/QueryEditor.tsx (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/datasource.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/img/logo.svg (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/module.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/package.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/plugin.json (100%) create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts create mode 100644 e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/tsconfig.json (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/types.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/variables.ts (100%) rename {e2e => e2e-playwright}/test-plugins/grafana-test-datasource/webpack.config.ts (100%) create mode 100644 e2e-playwright/utils/dashboard-helpers.ts create mode 100644 e2e-playwright/utils/prometheus-helpers.ts create mode 100644 e2e-playwright/various-suite/bar-gauge.spec.ts create mode 100644 e2e-playwright/various-suite/bookmarks.spec.ts create mode 100644 e2e-playwright/various-suite/exemplars.spec.ts create mode 100644 e2e-playwright/various-suite/explore.spec.ts create mode 100644 e2e-playwright/various-suite/filter-annotations.spec.ts create mode 100644 e2e-playwright/various-suite/frontend-sandbox-app.spec.ts create mode 100644 e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts create mode 100644 e2e-playwright/various-suite/gauge.spec.ts create mode 100644 e2e-playwright/various-suite/graph-auto-migrate.spec.ts create mode 100644 e2e-playwright/various-suite/inspect-drawer.spec.ts create mode 100644 e2e-playwright/various-suite/keybinds.spec.ts create mode 100644 e2e-playwright/various-suite/loki-query-builder.spec.ts create mode 100644 e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts create mode 100644 e2e-playwright/various-suite/migrate-to-cloud.spec.ts create mode 100644 e2e-playwright/various-suite/navigation.spec.ts create mode 100644 e2e-playwright/various-suite/pie-chart.spec.ts create mode 100644 e2e-playwright/various-suite/prometheus-annotations.spec.ts create mode 100644 e2e-playwright/various-suite/prometheus-config.spec.ts create mode 100644 e2e-playwright/various-suite/prometheus-editor.spec.ts create mode 100644 e2e-playwright/various-suite/prometheus-variable-editor.spec.ts create mode 100644 e2e-playwright/various-suite/query-editor.spec.ts create mode 100644 e2e-playwright/various-suite/return-to-previous.spec.ts create mode 100644 e2e-playwright/various-suite/solo-route.spec.ts create mode 100644 e2e-playwright/various-suite/trace-view-scrolling.spec.ts create mode 100644 e2e-playwright/various-suite/verify-i18n.spec.ts create mode 100644 e2e-playwright/various-suite/visualization-suggestions.spec.ts delete mode 100644 e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts delete mode 100644 e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts delete mode 100644 e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts delete mode 100644 e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts delete mode 100644 e2e/plugin-e2e/graphite/graphite.spec.ts delete mode 100644 e2e/plugin-e2e/influxdb/influxdb.spec.ts delete mode 100644 e2e/plugin-e2e/jaeger/jaeger.spec.ts delete mode 100644 e2e/plugin-e2e/mssql/mssql.spec.ts delete mode 100644 e2e/plugin-e2e/mysql/mysql.spec.ts delete mode 100644 e2e/plugin-e2e/mysql/visual-query-builder.spec.ts delete mode 100644 e2e/plugin-e2e/opentsdb/opentsdb.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts delete mode 100644 e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts delete mode 100644 e2e/plugin-e2e/postgres/postgres.spec.ts delete mode 100644 e2e/plugin-e2e/zipkin/zipkin.spec.ts delete mode 100644 e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts delete mode 100644 e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts delete mode 100644 e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts delete mode 100644 e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts delete mode 100644 e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts create mode 100644 pkg/build/e2e-playwright/README.md create mode 100644 pkg/build/e2e-playwright/e2e.go create mode 100644 pkg/build/e2e-playwright/frontend.go create mode 100644 pkg/build/e2e-playwright/main.go create mode 100644 pkg/build/e2e-playwright/service.go create mode 100644 playwright.storybook.config.ts diff --git a/.betterer.eslint.config.js b/.betterer.eslint.config.js index bbd5842a3c8..3b44630965e 100644 --- a/.betterer.eslint.config.js +++ b/.betterer.eslint.config.js @@ -35,7 +35,7 @@ module.exports = [ 'data/', 'deployment_tools_config.json', 'devenv', - 'e2e/test-plugins', + 'e2e-playwright/test-plugins', 'e2e/tmp', 'packages/grafana-ui/src/components/Icon/iconBundle.ts', 'pkg', diff --git a/.drone.yml b/.drone.yml index 9778f442148..bf621c24a1e 100644 --- a/.drone.yml +++ b/.drone.yml @@ -289,7 +289,7 @@ steps: - commands: - npx wait-on@7.0.1 http://$HOST:$PORT - yarn playwright install --with-deps chromium - - yarn e2e:playwright + - yarn e2e:playwright --grep @plugins depends_on: - grafana-server - build-test-plugins @@ -761,7 +761,7 @@ steps: - commands: - npx wait-on@7.0.1 http://$HOST:$PORT - yarn playwright install --with-deps chromium - - yarn e2e:playwright + - yarn e2e:playwright --grep @plugins depends_on: - grafana-server - build-test-plugins @@ -2986,6 +2986,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 1198b1489e48a9ced211633a0325d112814553246847fc7320fb5ac2bcb32b7d +hmac: d20f1d6e2e8347701f82114ad352f53db57dc95b5b3831941fa93d063a92b9d8 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec932f79bf3..1c05b89a30f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -400,8 +400,10 @@ /public/app/core/internationalization/ @grafana/grafana-frontend-platform /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources -/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend -/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend +/e2e-playwright/ @grafana/grafana-frontend-platform +/e2e-playwright/plugin-e2e/ @grafana/oss-big-tent @grafana/partner-datasources +/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend +/e2e-playwright/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend # Packages /packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend @@ -477,6 +479,7 @@ /cypress.config.js @grafana/grafana-frontend-platform /.levignore.js @grafana/plugins-platform-frontend playwright.config.ts @grafana/plugins-platform-frontend +playwright.storybook.config.ts @grafana/grafana-frontend-platform # public folder /public/app/api/ @grafana/grafana-frontend-platform @@ -770,6 +773,7 @@ embed.go @grafana/grafana-as-code /.github/pr-checks.json @tolzhabayev /.github/pr-commands.json @tolzhabayev /.github/renovate.json5 @grafana/frontend-ops +/.github/actions/check-jobs/action.yml @grafana/grafana-frontend-platform /.github/actions/setup-enterprise/action.yml @grafana/grafana-backend-group /.github/actions/setup-grafana-bench/ @Proximyst /.github/actions/build-package @grafana/grafana-developer-enablement-squad @@ -817,6 +821,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend /.github/workflows/stale.yml @grafana/grafana-developer-enablement-squad /.github/workflows/storybook-verification.yml @grafana/grafana-frontend-platform +/.github/workflows/storybook-verification-playwright.yml @grafana/grafana-frontend-platform /.github/workflows/update-make-docs.yml @grafana/docs-tooling /.github/workflows/scripts/kinds/verify-kinds.go @grafana/platform-monitoring /.github/workflows/scripts/create-security-branch/create-security-branch.sh @grafana/grafana-developer-enablement-squad diff --git a/.github/actions/check-jobs/action.yml b/.github/actions/check-jobs/action.yml new file mode 100644 index 00000000000..b4551730b96 --- /dev/null +++ b/.github/actions/check-jobs/action.yml @@ -0,0 +1,48 @@ +name: Check jobs results +description: Checks if any jobs have failed and exits with error if failures are found. Use to check the results of matrix test runs. +inputs: + needs: + description: JSON string containing the needs context from the workflow + required: true + failure-message: + description: Custom message to display when failures are found + required: false + default: "One or more jobs have failed" + success-message: + description: Custom message to display when all jobs pass + required: false + default: "All jobs passed successfully" +outputs: + any-failed: + description: Whether any jobs failed + value: ${{ steps.check-jobs.outputs.any-failed }} + +runs: + using: "composite" + steps: + - name: Check test suites + id: check-jobs + shell: bash + env: + NEEDS: ${{ inputs.needs }} + FAILURE_MSG: ${{ inputs.failure-message }} + SUCCESS_MSG: ${{ inputs.success-message }} + run: | + set -euo pipefail + + # Print the needs context, debugging + echo "$NEEDS" | jq + + # Extract failures + FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')" + + # Check if there are any failures + if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then + echo "❌ $FAILURE_MSG" + echo "Failed suites:" + echo "$FAILURES" | jq -r 'to_entries[] | "- \(.key): \(.value)"' + echo "any-failed=true" >> "$GITHUB_OUTPUT" + exit 1 + fi + + echo "✅ $SUCCESS_MSG" diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index ec74716f7e3..5e7d116d0c2 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -7,12 +7,17 @@ on: - main - release-*.*.* -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} +# TODO: re-enable this before merging +# concurrency: +# group: ${{ github.workflow }}-${{ github.ref }} +# cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} permissions: {} +env: + ACTIONS_STEP_DEBUG: true + RUNNER_DEBUG: 1 + jobs: detect-changes: name: Detect whether code changed @@ -44,22 +49,56 @@ jobs: steps: - uses: actions/checkout@v4 with: - path: ./grafana persist-credentials: false - - uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + + # TODO: add a cleanup workflow to remove the cache when the PR is closed + # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy + # TODO: maybe we could just use the cache to store the build, instead of uploading as an artifact? + - uses: actions/cache@v4 + id: cache + with: + key: "build-grafana-${{ runner.os }}-${{ hashFiles('yarn.lock', 'public/*', 'packages/*', 'pkg/**/*.go', '**/go.mod', '**/go.sum', '!**_test.go', '!**.test.ts', '!**.test.tsx') }}" + path: | + build-dir + + # If no cache hit, build Grafana + - name: Build Grafana + if: steps.cache.outputs.cache-hit != 'true' + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e with: verb: run - args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt - - run: mv "$(cat out.txt)" grafana.tar.gz - - run: echo "artifact=grafana-e2e-${{github.run_number}}" >> "$GITHUB_OUTPUT" + args: go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}" > out.txt + - name: Cat built artifact + if: steps.cache.outputs.cache-hit != 'true' + run: cat out.txt + - name: Move built artifact + if: steps.cache.outputs.cache-hit != 'true' + run: | + mkdir -p build-dir + mv "$(cat out.txt)" build-dir/grafana.tar.gz + + # If cache hit, validate the artifact is present + - name: Validate artifact + if: steps.cache.outputs.cache-hit == 'true' + run: | + if [ ! -f build-dir/grafana.tar.gz ]; then + echo "Error: build-dir/grafana.tar.gz not found in cache" + exit 1 + fi + + - name: Set artifact name + run: echo "artifact=grafana-server-${{github.run_number}}" >> "$GITHUB_OUTPUT" id: artifact - - uses: actions/upload-artifact@v4 + + - name: Upload artifact + uses: actions/upload-artifact@v4 id: upload with: retention-days: 1 name: ${{ steps.artifact.outputs.artifact }} - path: grafana.tar.gz + path: build-dir/grafana.tar.gz + # TODO: we won't need this when we only have playwright build-e2e-runner: needs: detect-changes if: needs.detect-changes.outputs.changed == 'true' @@ -159,6 +198,119 @@ jobs: path: videos retention-days: 1 + run-playwright-tests: + needs: + - build-grafana + name: Playwright E2E tests (${{ matrix.shard }}/${{ matrix.shardTotal }}) + runs-on: ubuntu-latest-8-cores + permissions: + contents: read + + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8] + shardTotal: [8] + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.build-grafana.outputs.artifact }} + - name: Run E2E tests + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + with: + verb: run + args: go run ./pkg/build/e2e-playwright --package=grafana.tar.gz --shard=${{ matrix.shard }}/${{ matrix.shardTotal }} --blob-dir=./blob-report + - uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: playwright-blob-${{ github.run_number }}-${{ matrix.shard }} + path: ./blob-report + retention-days: 1 + + required-playwright-tests: + needs: + - run-playwright-tests + - build-grafana + if: ${{ !cancelled() }} + name: All Playwright tests complete + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: blobs + pattern: playwright-blob-* + merge-multiple: true + + - name: Check blob reports + run: | + if [ ! "$(ls -A ./blobs)" ]; then + echo "Error: No blob reports found in ./blobs directory" + echo "Did the Playwright tests run at all?" + exit 1 + fi + echo "Found blob reports in ./blobs:" + ls -lah ./blobs + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./blobs + + - name: Merge into JSON Report + env: + PLAYWRIGHT_JSON_OUTPUT_NAME: /tmp/playwright-results.json + run: npx playwright merge-reports --reporter=json ./blobs + + - name: Bench report + run: | + docker run --rm \ + --volume="/tmp/playwright-results.json:/home/bench/tests/playwright-results.json" \ + us-docker.pkg.dev/grafanalabs-global/docker-grafana-bench-prod/grafana-bench:v0.5.1 report \ + --grafana-url "http://localhost:3000" \ + --grafana-version "CI- ${{ github.sha }}" \ + --test-suite-name "FrontendCore" \ + --report-input playwright \ + --report-output log \ + --log-level DEBUG \ + /home/bench/tests/playwright-results.json + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: playwright-html-${{ github.run_number }} + path: playwright-report + retention-days: 7 + + - name: Check test suites + id: check-jobs + uses: ./.github/actions/check-jobs + continue-on-error: true # Failure will be reported on Show test results step + with: + needs: ${{ toJson(needs) }} + failure-message: "One or more E2E test suites have failed" + success-message: "All E2E test suites completed successfully" + + - name: Show test results + env: + FAILED: ${{ steps.check-jobs.outputs.any-failed }} + # sed removes the leading `../../src/` from the paths + run: | + npx playwright merge-reports --reporter list ./all-blob-reports | sed 's|\(\.\./\)\{1,\}src/|/|g' + if [ "$FAILED" = "true" ]; then + exit 1 + fi + run-a11y-test: needs: - build-grafana @@ -193,6 +345,7 @@ jobs: required-e2e-tests: needs: - run-e2e-tests + - build-grafana # a11y test is not listed on purpose: it is not an important E2E test. # It is also totally fine to fail right now. # always() is the best function here. @@ -203,13 +356,13 @@ jobs: name: All E2E tests complete runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Check test suites - env: - NEEDS: ${{ toJson(needs) }} - run: | - FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')" - echo "$FAILURES" - if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then - exit 1 - fi - echo "All OK!" + uses: ./.github/actions/check-jobs + with: + needs: ${{ toJson(needs) }} + failure-message: "One or more E2E test suites have failed" + success-message: "All E2E test suites completed successfully" diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 06a644f5843..2c43ae43de4 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -208,7 +208,7 @@ jobs: if: ${{ inputs.bump == true || inputs.bump == 'true' }} run: | git add package.json lerna.json yarn.lock packages public - test -e e2e/test-plugins && git add e2e/test-plugins + test -e e2e-playwright/test-plugins && git add e2e-playwright/test-plugins git commit -m "Update version to $VERSION" - name: Git push diff --git a/.github/workflows/run-dashboard-search-e2e.yml b/.github/workflows/run-dashboard-search-e2e.yml index 8554f7549a6..6eebaf9320a 100644 --- a/.github/workflows/run-dashboard-search-e2e.yml +++ b/.github/workflows/run-dashboard-search-e2e.yml @@ -64,7 +64,7 @@ jobs: tools/ public/ conf/ - e2e/test-plugins/ + e2e-playwright/test-plugins/ devenv/ key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }} # only rebuild grafana if search files have changed ( or dependencies ) @@ -116,7 +116,7 @@ jobs: tools/ public/ conf/ - e2e/test-plugins/ + e2e-playwright/test-plugins/ devenv/ key: ${{ runner.os }}-grafana-${{ hashFiles('go.mod', 'package-lock.json', 'Makefile', 'pkg/storage/**/*.go', 'public/app/features/search/**/*.ts', 'public/app/features/search/**/*.tsx') }} - name: Set the step name diff --git a/.github/workflows/storybook-verification-playwright.yml b/.github/workflows/storybook-verification-playwright.yml new file mode 100644 index 00000000000..e8391259f55 --- /dev/null +++ b/.github/workflows/storybook-verification-playwright.yml @@ -0,0 +1,45 @@ +name: Verify Storybook (Playwright) + +on: + pull_request: + paths: + - 'packages/grafana-ui/**' + - '!docs/**' + - '!*.md' + push: + branches: + - main + paths: + - 'packages/grafana-ui/**' + - '!docs/**' + - '!*.md' + +permissions: {} + +jobs: + verify-storybook: + name: Verify Storybook (Playwright) + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --immutable + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Run Storybook and E2E tests + run: yarn e2e:playwright:storybook diff --git a/.gitignore b/.gitignore index ccc42239a87..7457c8148b3 100644 --- a/.gitignore +++ b/.gitignore @@ -237,4 +237,4 @@ public/app/plugins/**/dist/ # Mock service worker used for fake API responses in frontend development public/mockServiceWorker.js -/e2e/test-plugins/*/dist +/e2e-playwright/test-plugins/*/dist diff --git a/contribute/style-guides/e2e-plugins.md b/contribute/style-guides/e2e-plugins.md index e708281d675..8d6ffaca61a 100644 --- a/contribute/style-guides/e2e-plugins.md +++ b/contribute/style-guides/e2e-plugins.md @@ -6,9 +6,9 @@ When end-to-end testing Grafana plugins, a best practice is to use the [`@grafan ## Add end-to-end tests for a core plugin -You can add Playwright end-to-end tests for plugins to the [`e2e/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e) directory. +You can add Playwright end-to-end tests for plugins to the [`e2e-playwright/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e) directory. -1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e). This is the directory where your plugin tests will be kept. +1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e-playwright/plugin-e2e). This is the directory where your plugin tests will be kept. 1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration. In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests. diff --git a/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts b/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts new file mode 100644 index 00000000000..29cc753df75 --- /dev/null +++ b/e2e-playwright/cloud-plugins-suite/azure-monitor.spec.ts @@ -0,0 +1,544 @@ +import { readFileSync } from 'fs'; +import { load } from 'js-yaml'; +import { Page } from 'playwright-core'; +import { v4 as uuidv4 } from 'uuid'; + +import { + test, + expect, + CreateDataSourcePageArgs, + DataSourceConfigPage, + E2ESelectorGroups, + DashboardPage, +} from '@grafana/plugin-e2e'; + +import { AzureQueryType } from '../../public/app/plugins/datasource/azuremonitor/dataquery.gen'; +import { selectors as azMonSelectors } from '../../public/app/plugins/datasource/azuremonitor/e2e/selectors'; +import { + AzureMonitorDataSourceJsonData, + AzureMonitorDataSourceSecureJsonData, +} from '../../public/app/plugins/datasource/azuremonitor/types/types'; + +const provisioningPath = 'provisioning/datasources/azmonitor-ds.yaml'; + +type AzureMonitorConfig = { + secureJsonData: AzureMonitorDataSourceSecureJsonData; + jsonData: AzureMonitorDataSourceJsonData; +}; + +type AzureMonitorProvision = { datasources: AzureMonitorConfig[] }; + +const dataSourceName = `Azure Monitor E2E Tests - ${uuidv4()}`; +const storageAcctName = 'azmonteststorage'; +const logAnalyticsName = 'az-mon-test-logs'; +const applicationInsightsName = 'az-mon-test-ai-a'; +const rootSubscription = 'grafanalabs-datasources-dev'; + +async function provisionAzureMonitorDatasources( + createDataSourceConfigPage: (args: CreateDataSourcePageArgs) => Promise, + datasourceConfig: AzureMonitorConfig, + page: Page, + selectors: E2ESelectorGroups +) { + const configPage = await createDataSourceConfigPage({ + type: 'grafana-azure-monitor-datasource', + name: dataSourceName, + }); + + const azureCloudInput = page.getByTestId(azMonSelectors.components.configEditor.azureCloud.input).locator('input'); + await azureCloudInput.fill('Azure'); + await azureCloudInput.press('Enter'); + const tenantIdInput = page.getByTestId(azMonSelectors.components.configEditor.tenantID.input).locator('input'); + await tenantIdInput.fill(datasourceConfig.jsonData.tenantId!); + const clientIdInput = page.getByTestId(azMonSelectors.components.configEditor.clientID.input).locator('input'); + await clientIdInput.fill(datasourceConfig.jsonData.clientId!); + const clientSecretInput = page + .getByTestId(azMonSelectors.components.configEditor.clientSecret.input) + .locator('input'); + await clientSecretInput.fill(datasourceConfig.secureJsonData.clientSecret!); + const loadSubscriptionsButton = page.getByTestId(azMonSelectors.components.configEditor.loadSubscriptions.button); + await loadSubscriptionsButton.click(); + const defaultSubscriptionInput = page + .getByTestId(azMonSelectors.components.configEditor.defaultSubscription.input) + .locator('input'); + await defaultSubscriptionInput.fill('datasources'); + await configPage + .getByGrafanaSelector(selectors.components.Select.option) + .filter({ + hasText: 'datasources', + }) + .click(); + await configPage.saveAndTest(); +} + +// TODO unskip when we've figured out how to populate the credentials in CI +test.describe.skip( + 'Azure Monitor datasource', + { + tag: ['@cloud-plugins'], + }, + () => { + let datasourceConfig: AzureMonitorConfig; + + test.beforeAll(async () => { + // Check if we're running in CI + const CI = process.env.CI; + if (CI) { + const outputs = JSON.parse(readFileSync('outputs.json', 'utf8')); + datasourceConfig = { + jsonData: { + cloudName: 'Azure', + tenantId: outputs.tenantId, + clientId: outputs.clientId, + }, + secureJsonData: { clientSecret: outputs.clientSecret }, + }; + } else { + const yamlContent = readFileSync(provisioningPath, 'utf8'); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const yaml = load(yamlContent) as AzureMonitorProvision; + datasourceConfig = yaml.datasources[0]; + } + }); + + test('create dashboard, add panel for metrics, log analytics, ARG, and traces queries', async ({ + createDataSourceConfigPage, + gotoDashboardPage, + selectors, + page, + }) => { + // this test can absolutely take longer than the default 30s timeout + test.setTimeout(120000); + await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors); + // Create new dashboard + const dashboardPage = await gotoDashboardPage({ + timeRange: { + from: 'now-6h', + to: 'now', + zone: 'Coordinated Universal Time', + }, + }); + + // Add metrics panel + const metricsPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + const datasourcePicker = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill(dataSourceName); + const datasourceList = metricsPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText(dataSourceName).click(); + + // Configure metrics query + const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button); + await resourcePickerButton.click(); + await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 }); + const resourceSearchInput = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.search.input); + await resourceSearchInput.fill(storageAcctName); + await expect(page.getByText(storageAcctName)).toBeVisible({ timeout: 30000 }); + await page.getByText(storageAcctName).click(); + const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button); + await applyButton.click(); + await expect(page.getByText('microsoft.storage/storageaccounts')).toBeVisible(); + const metricNameInput = page + .getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input) + .locator('input'); + await metricNameInput.fill('Used capacity'); + await metricNameInput.press('Enter'); + + // Save and go back to dashboard + metricsPanel.backToDashboard(); + + // Add logs panel + const logsPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + await datasourcePicker.fill(dataSourceName); + await datasourceList.getByText(dataSourceName).click(); + + // Switch to Logs query type + const queryTypeSelect = page.getByTestId(azMonSelectors.components.queryEditor.header.select).locator('input'); + await queryTypeSelect.fill('Logs'); + await queryTypeSelect.press('Enter'); + + // Configure logs query + await resourcePickerButton.click(); + await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 }); + await resourceSearchInput.fill(logAnalyticsName); + await expect(page.getByText(logAnalyticsName)).toBeVisible({ timeout: 30000 }); + await page.getByText(logAnalyticsName).click(); + await applyButton.click(); + let codeEditor = logsPanel.getByGrafanaSelector(selectors.components.CodeEditor.container).locator('textarea'); + await codeEditor.fill('AzureDiagnostics', { force: true }); + const formatSelection = page + .getByTestId(azMonSelectors.components.queryEditor.logsQueryEditor.formatSelection.input) + .locator('input'); + await formatSelection.fill('Time series'); + await formatSelection.press('Enter'); + + // Save and go back to dashboard + logsPanel.backToDashboard(); + + // Add Azure Resource Graph panel + const resourceGraphPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + await datasourcePicker.fill(dataSourceName); + await datasourceList.getByText(dataSourceName).click(); + + // Switch to Azure Resource Graph query type + await queryTypeSelect.fill('Azure Resource Graph'); + await queryTypeSelect.press('Enter'); + + // Configure Azure Resource Graph query + const subscriptionsInput = page + .getByTestId(azMonSelectors.components.queryEditor.argsQueryEditor.subscriptions.input) + .locator('input'); + await subscriptionsInput.fill('datasources'); + await subscriptionsInput.press('Enter'); + codeEditor = resourceGraphPanel + .getByGrafanaSelector(selectors.components.CodeEditor.container) + .locator('textarea'); + await codeEditor.fill( + "Resources | where resourceGroup == 'cloud-plugins-e2e-test-azmon' | project name, resourceGroup" + ); + + // Save and go back to dashboard + resourceGraphPanel.backToDashboard(); + + // Add traces panel + const tracesPanel = await dashboardPage.addPanel(); + + // Select Azure Monitor datasource + await datasourcePicker.fill(dataSourceName); + await datasourceList.getByText(dataSourceName).click(); + + // Switch to Traces query type + await queryTypeSelect.fill('Traces'); + await queryTypeSelect.press('Enter'); + + // Configure traces query + await resourcePickerButton.click(); + await expect(page.getByText(rootSubscription)).toBeVisible({ timeout: 30000 }); + await resourceSearchInput.fill(applicationInsightsName); + await expect(page.getByText(applicationInsightsName)).toBeVisible({ timeout: 30000 }); + await page.getByText(applicationInsightsName).click(); + await applyButton.click(); + await formatSelection.fill('Trace'); + await formatSelection.press('Enter'); + }); + + test('create dashboard with template variables', async ({ + createDataSourceConfigPage, + gotoDashboardPage, + page, + selectors, + }) => { + await provisionAzureMonitorDatasources(createDataSourceConfigPage, datasourceConfig, page, selectors); + // Create new dashboard + const dashboardPage = await gotoDashboardPage({ + timeRange: { + from: 'now-6h', + to: 'now', + }, + }); + + // Add subscription variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'subscription', + AzureQueryType.SubscriptionsQuery, + true + ); + + // Add resource groups variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'resourceGroups', + AzureQueryType.ResourceGroupsQuery, + false, + { + subscription: '$subscription', + } + ); + + // Add namespaces variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'namespaces', + AzureQueryType.NamespacesQuery, + false, + { + subscription: '$subscription', + resourceGroup: '$resourceGroups', + } + ); + + // Add region variable + await addAzureMonitorVariable(page, dashboardPage, selectors, 'region', AzureQueryType.LocationsQuery, false, { + subscription: '$subscription', + }); + + // Add resource variable + await addAzureMonitorVariable( + page, + dashboardPage, + selectors, + 'resource', + AzureQueryType.ResourceNamesQuery, + false, + { + subscription: '$subscription', + resourceGroup: '$resourceGroups', + namespace: '$namespaces', + region: '$region', + } + ); + + // Set variable values + const subscriptionVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('subscription')) + .locator('..') + .locator('input'); + await subscriptionVariable.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .filter({ hasText: 'grafanalabs-datasources-dev' }) + .click(); + + const resourceGroupsVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resourceGroups')) + .locator('..') + .locator('input'); + await resourceGroupsVariable.fill('cloud-plugins-e2e-test-azmon'); + await resourceGroupsVariable.press('ArrowDown'); + await resourceGroupsVariable.press('Enter'); + + const namespacesVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('namespaces')) + .locator('..') + .locator('input'); + await namespacesVariable.fill('microsoft.storage/storageaccounts'); + await namespacesVariable.press('ArrowDown'); + await namespacesVariable.press('Enter'); + + const regionVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('region')) + .locator('..') + .locator('input'); + await regionVariable.fill('uk south'); + await regionVariable.press('ArrowDown'); + await regionVariable.press('Enter'); + + const resourceVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('resource')) + .locator('..') + .locator('input'); + await resourceVariable.fill(storageAcctName); + await resourceVariable.press('ArrowDown'); + await resourceVariable.press('Enter'); + + // Add panel with template variables + const newPanel = await dashboardPage.addPanel(); + + const datasourcePicker = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill(dataSourceName); + const datasourceList = newPanel.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText(dataSourceName).click(); + + const resourcePickerButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.select.button); + await resourcePickerButton.click(); + const advancedCollapse = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.collapse); + await advancedCollapse.click(); + const advancedSubscription = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.subscription.input) + .locator('input'); + await advancedSubscription.fill('$subscription'); + const advancedResourceGroup = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resourceGroup.input) + .locator('input'); + await advancedResourceGroup.fill('$resourceGroups'); + const advancedNamespace = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.namespace.input) + .locator('input'); + await advancedNamespace.fill('$namespaces'); + const advancedRegion = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.region.input) + .locator('input'); + await advancedRegion.fill('$region'); + const advancedResource = page + .getByTestId(azMonSelectors.components.queryEditor.resourcePicker.advanced.resource.input) + .locator('input'); + await advancedResource.fill('$resource'); + + const applyButton = page.getByTestId(azMonSelectors.components.queryEditor.resourcePicker.apply.button); + await applyButton.click(); + + const metricNameInput = page + .getByTestId(azMonSelectors.components.queryEditor.metricsQueryEditor.metricName.input) + .locator('input'); + await metricNameInput.fill('Transactions'); + await metricNameInput.press('Enter'); + }); + + // Helper function to add template variables + async function addAzureMonitorVariable( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + name: string, + type: AzureQueryType, + isFirst: boolean, + options?: { + subscription?: string; + resourceGroup?: string; + namespace?: string; + resource?: string; + region?: string; + } + ) { + // Navigate to variables settings + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.settingsButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Variables')).click(); + + if (isFirst) { + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.List.addVariableCTAV2) + .click(); + } else { + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + } + + // Configure variable + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.clear(); + await nameInput.fill(name); + + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill(dataSourceName); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText(dataSourceName).click(); + + const queryTypeInput = page + .getByTestId(azMonSelectors.components.variableEditor.queryType.input) + .locator('input'); + await queryTypeInput.fill(type.replace('Azure', '').trim()); + await queryTypeInput.press('Enter'); + + // Configure type-specific options + switch (type) { + case AzureQueryType.ResourceGroupsQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + break; + case AzureQueryType.LocationsQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + break; + case AzureQueryType.NamespacesQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + if (options?.resourceGroup) { + const resourceGroupInput = page + .getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input) + .locator('input'); + await resourceGroupInput.fill(options.resourceGroup); + await resourceGroupInput.press('Enter'); + } + break; + case AzureQueryType.ResourceNamesQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + if (options?.resourceGroup) { + const resourceGroupInput = page + .getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input) + .locator('input'); + await resourceGroupInput.fill(options.resourceGroup); + await resourceGroupInput.press('Enter'); + } + if (options?.namespace) { + const namespaceInput = page + .getByTestId(azMonSelectors.components.variableEditor.namespace.input) + .locator('input'); + await namespaceInput.fill(options.namespace); + await namespaceInput.press('Enter'); + } + if (options?.region) { + const regionInput = page + .getByTestId(azMonSelectors.components.variableEditor.region.input) + .locator('input'); + await regionInput.fill(options.region); + await regionInput.press('Enter'); + } + break; + case AzureQueryType.MetricNamesQuery: + if (options?.subscription) { + const subscriptionInput = page + .getByTestId(azMonSelectors.components.variableEditor.subscription.input) + .locator('input'); + await subscriptionInput.fill(options.subscription); + await subscriptionInput.press('Enter'); + } + if (options?.resourceGroup) { + const resourceGroupInput = page + .getByTestId(azMonSelectors.components.variableEditor.resourceGroup.input) + .locator('input'); + await resourceGroupInput.fill(options.resourceGroup); + await resourceGroupInput.press('Enter'); + } + if (options?.namespace) { + const namespaceInput = page + .getByTestId(azMonSelectors.components.variableEditor.namespace.input) + .locator('input'); + await namespaceInput.fill(options.namespace); + await namespaceInput.press('Enter'); + } + if (options?.resource) { + const resourceInput = page + .getByTestId(azMonSelectors.components.variableEditor.resource.input) + .locator('input'); + await resourceInput.fill(options.resource); + await resourceInput.press('Enter'); + } + break; + } + + // Save variable + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton) + .click(); + + // Go back to dashboard + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + } + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts new file mode 100644 index 00000000000..f42082587d8 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-duplicate-panel.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; + +import { flows } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +test.describe( + 'Dashboard panels', + { + tag: ['@dashboards'], + }, + () => { + test('can duplicate a panel', async ({ dashboardPage, selectors, page }) => { + await page.goto(selectors.pages.ImportDashboard.url); + await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); + await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); + await page.getByTestId(selectors.components.ImportDashboardForm.name).fill('Paste tab'); + await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + const oldPanelTitle = 'New panel'; + const panelTitle = 'Unique'; + await flows.changePanelTitle(dashboardPage, selectors, oldPanelTitle, panelTitle); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle)) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.menu(panelTitle)) + .click({ force: true }); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('More...')).hover(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Duplicate')).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount( + 2 + ); + + // Save, reload, and ensure duplicate has persisted + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click(); + await page.reload(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(panelTitle))).toHaveCount( + 2 + ); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts new file mode 100644 index 00000000000..80f94f855b2 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -0,0 +1,804 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +// these tests require a larger viewport +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +test.describe( + 'Grouping panels', + { + tag: ['@dashboards'], + }, + () => { + // Helper functions + async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) { + await page.goto(selectors.pages.ImportDashboard.url); + await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); + await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); + await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title); + await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); + } + + async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click(); + } + + async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into row').click(); + } + + async function groupIntoTab(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into tab').click(); + } + + async function ungroupPanels(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.ungroup).click(); + } + + async function addPanel(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click(); + } + + /* + * Rows + */ + + test('can group and ungroup new panels into row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into row + await groupIntoRow(page, dashboardPage, selectors); + + // Verify row and panel titles + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify row and panel titles after reload + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can add and remove several rows', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Add and remove rows'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click(); + await addPanel(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(5); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(5); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + .locator('..') + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + .locator('..') + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 2')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can paste a copied row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Paste row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Copy by selecting row and using copy button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Copy' }).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteRow).click(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // scroll last `New panel` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + .last() + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + }); + + test('can duplicate a row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Duplicate row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Duplicate by selecting row and using duplicate button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // scroll last `New panel` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + .last() + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + }); + + test('can collapse rows', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Collapse rows'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Duplicate row + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(6); + + // Collapse rows by clicking on their titles + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(0); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(0); + }); + + test('can convert rows into tabs when changing layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Rows to tabs'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + + // Duplicate row + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1')) + ).toBeVisible(); + + // Go back to dashboard options + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + + // Expand layouts section + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category')) + .click(); + + // Select tabs layout + await page.getByLabel('Tabs').click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1')).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row')).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can group and ungroup new panels into row with tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into tab with row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into row with tab + await groupIntoRow(page, dashboardPage, selectors); + await groupIntoTab(page, dashboardPage, selectors); + + // Verify tab and panel titles + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify tab, row and panel titles after reload + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); // ungroup tabs + await ungroupPanels(dashboardPage, selectors); // ungroup rows + + // Verify tab and row titles is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + /* + * Tabs + */ + + test('can group and ungroup new panels into tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into tab'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into tab + await groupIntoTab(page, dashboardPage, selectors); + + // Verify tab and panel titles + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify row and panel titles after reload + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can add and remove several tabs', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Add and remove tabs'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click(); + await addPanel(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click(); + await addPanel(dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute( + 'aria-selected', + 'true' + ); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(1); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toHaveAttribute( + 'aria-selected', + 'true' + ); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(1); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can paste a copied tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Paste tab'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + // Copy by selecting tab and using copy button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Copy' }).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.pasteTab).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can duplicate a tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Duplicate tab'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + // Duplicate by selecting tab and using duplicate button + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + + test('can convert tabs into rows when changing layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Tabs to rows'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + // Duplicate tab twice + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.copyDropdown).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 2'))).toBeVisible(); + + // Go back to dashboard options + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click({ force: true }); + + // Expand layouts section + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('group-layout-category')) + .click(); + + // Select rows layout + await page.getByLabel('Rows').click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab')) + ).toBeVisible(); + // Wait for panels to load + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first() + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + ).toBeVisible(); + + // scroll `New row` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow) + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(9); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab')) + ).toBeVisible(); + // Wait for panels to load + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first() + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 1')) + ).toBeVisible(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + .scrollIntoViewIfNeeded(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New tab 2')) + ).toBeVisible(); + + // scroll last `New panel` into view - this is at the bottom of the dashboard body + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + .last() + .scrollIntoViewIfNeeded(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(9); + }); + + test('can group and ungroup new panels into tab with row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Group new panels into tab with row'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Group into tab + await groupIntoTab(page, dashboardPage, selectors); + await groupIntoRow(page, dashboardPage, selectors); + + // Verify tab and panel titles + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify tab, row and panel titles after reload + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Ungroup + await ungroupPanels(dashboardPage, selectors); // ungroup rows + await ungroupPanels(dashboardPage, selectors); // ungroup tabs + + // Verify tab and row titles is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + // Save dashboard and reload + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + // Verify Row title is gone + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts new file mode 100644 index 00000000000..c2214121d85 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-outline.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +test.describe( + 'Dashboard Outline', + { + tag: ['@dashboards'], + }, + () => { + test('can use dashboard outline', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); + + // Should be able to click Variables item in outline to see add variable button + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) + ).toBeVisible(); + + // Clicking a panel should scroll that panel in view + await expect(page.getByText('Dashboard panel 48')).toBeHidden(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Panel #48')).click(); + await expect(page.getByText('Dashboard panel 48')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts new file mode 100644 index 00000000000..d0b4cb5fcaf --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-add-panel.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard panels', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new panel', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel).last().click(); + + // Check that new panel has been added + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + + // Check that pressing the configure button shows the panel editor + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.content) + .filter({ hasText: 'Configure' }) + .click(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts new file mode 100644 index 00000000000..01a3e65db22 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-adhoc-variables.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - Ad hoc variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new adhoc variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'adhoc', + name: 'VariableUnderTest', + value: 'label1', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // Select datasource for the ad hoc variable + const dataSource = 'gdev-loki'; + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.AdHocFiltersVariable.datasourceSelect) + .click(); + await page.getByText(dataSource).click(); + + // mock the API call to get the labels + const labels = ['label1', 'label2']; + await page.route('**/resources/labels*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labels, + }), + }); + }); + + // Select the variable in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // mock the API call to get the label values + const labelValues = ['label2Value1']; + await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labelValues, + }), + }); + }); + + // choose the label and value + await page.getByText(labels[1]).click(); + await page.getByText('=', { exact: true }).click(); + await page.getByText(labelValues[0]).click(); + await page.keyboard.press('Escape'); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}="${labelValues[0]}"`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts new file mode 100644 index 00000000000..14af22ab2c3 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-datasource-variables.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - datasource variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new datasource variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const dsType = 'cloudwatch'; + const variable: Variable = { + type: 'datasource', + name: 'VariableUnderTest', + label: 'VariableUnderTest', + value: `gdev-${dsType}`, + }; + + // Common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect) + .click(); + await page.getByText(dsType).click(); + + const regexFilter = 'cloud'; + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.nameFilter) + .fill(regexFilter); + + // Assert the variable dropdown is visible with correct label + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // Assert the variable values are correctly displayed in the panel + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`${variable.name}: ${variable.value}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts new file mode 100644 index 00000000000..e964414dcad --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-group-by-variables.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - Group By variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new group by variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'groupby', + name: 'VariableUnderTest', + value: 'label1', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // select datasource for the group by variable + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.GroupByVariable.dataSourceSelect) + .click(); + const dataSource = 'gdev-loki'; + await page.getByText(dataSource).click(); + + // mock the API call to get the labels + const labels = ['label1', 'label2']; + await page.route('**/resources/labels*', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labels, + }), + }); + }); + + // open the variable dropdown in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).locator('> div').click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // mock the API call to get the label values + const labelValues = ['label2Value1']; + await page.route(`**/resources/label/${labels[1]}/values*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: labelValues, + }), + }); + }); + + // choose the label and value + await page.getByText(labels[1]).click(); + await page.keyboard.press('Escape'); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${labels[1]}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts new file mode 100644 index 00000000000..30373362528 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph'; + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can edit panel title and description', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + const oldTitle = 'No Data Points Warning'; + const firstPanelTitle = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .first() + .locator('h2') + .first(); + await expect(firstPanelTitle).toHaveText(oldTitle); + + const newDescription = 'A description of this panel'; + await flows.changePanelDescription(dashboardPage, selectors, oldTitle, newDescription); + + const newTitle = 'New Panel Title'; + await flows.changePanelTitle(dashboardPage, selectors, oldTitle, newTitle); + + // Check that new title is reflected in panel header + const updatedPanelTitle = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .first() + .locator('h2') + .first(); + await expect(updatedPanelTitle).toHaveText(newTitle); + + // Reveal description tooltip and check that its value is as expected + const descriptionIcon = page.locator('[data-testid="title-items-container"] > span').first(); + await descriptionIcon.click({ force: true }); + + // Get the tooltip ID from the aria-describedby attribute + const tooltipId = await descriptionIcon.getAttribute('aria-describedby'); + const tooltip = page.locator(`[id="${tooltipId}"]`); + await expect(tooltip).toHaveText(`${newDescription}\n`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts new file mode 100644 index 00000000000..d483b7b8005 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-transparent-bg.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = '5SdHCadmz/panel-tests-graph'; + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can toggle transparent background switch', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^No Data Points Warning$/ }) + .first() + .click(); + + const panelTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('No Data Points Warning') + ); + + const initialBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background); + expect(initialBackground).not.toMatch(/rgba\(0, 0, 0, 0\)/); + + await page.locator('#transparent-background').click({ force: true }); + + const transparentBackground = await panelTitle.evaluate((el) => getComputedStyle(el).background); + expect(transparentBackground).toMatch(/rgba\(0, 0, 0, 0\)/); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts new file mode 100644 index 00000000000..6c55988a309 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-query-variables.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - Query variable', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new query variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const queryVariableOptions = ['default']; + + const variable: Variable = { + type: 'query', + name: 'VariableUnderTest', + value: queryVariableOptions[0], + label: 'VariableUnderTest', // constant doesn't really need a label + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // open the modal query variable editor + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsOpenButton) + .click(); + // select a core data source that just runs a query during preview + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + + const dataSource = 'gdev-cloudwatch'; + // this will trigger an API call to get the query options + await page.getByText(dataSource).click(); + + // show the preview of the query results + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.previewButton) + .click(); + // assert the query results are shown + const firstPreviewOption = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption) + .first(); + await expect(firstPreviewOption).toBeVisible(); + const previewOptionText = await firstPreviewOption.textContent(); + const previewOption = previewOptionText?.trim() || ''; + + // close the modal + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.closeButton) + .click(); + + // assert the query variable values are in the variable value select + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!) + ); + const nextElement = variableLabel.locator('+ *'); + await expect(nextElement).toHaveText(previewOption); + + // Assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${previewOption}`); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts new file mode 100644 index 00000000000..411d2122954 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-variables.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { flows, type Variable } from './utils'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Dashboard edit - variables', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new custom variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'custom', + name: 'foo', + label: 'Foo', + value: 'one,two,three', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // set the custom variable value + const customValueInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput + ); + await customValueInput.fill(variable.value); + await customValueInput.blur(); + + // assert the dropdown for the variable is visible and has the correct values + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + const values = variable.value.split(','); + const firstValueLink = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(values[0]) + ); + await expect(firstValueLink).toBeVisible(); + + // check that variable deletion works + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await expect(variableLabel).toBeHidden(); + }); + + test('can add a new constant variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'constant', + name: 'VariableUnderTest', + value: 'foo', + label: 'VariableUnderTest', // constant doesn't really need a label + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // set the constant variable value + const type = 'variable-type Value'; + const fieldLabel = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(type) + ); + await expect(fieldLabel).toBeVisible(); + const inputField = fieldLabel.locator('input'); + await expect(inputField).toBeVisible(); + await inputField.fill(variable.value); + await inputField.blur(); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`); + }); + + test('can add a new textbox variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'textbox', + name: 'VariableUnderTest', + value: 'foo', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // set the textbox variable value + const type = 'variable-type Value'; + const fieldLabel = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(type) + ); + await expect(fieldLabel).toBeVisible(); + const inputField = fieldLabel.locator('input'); + await expect(inputField).toBeVisible(); + await inputField.fill(variable.value); + await inputField.blur(); + + // select the variable in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`); + }); + + test('can add a new interval variable', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + const variable: Variable = { + type: 'interval', + name: 'VariableUnderTest', + value: '1m', + label: 'VariableUnderTest', + }; + + // common steps to add a new variable + await flows.newEditPaneVariableClick(dashboardPage, selectors); + await flows.newEditPanelCommonVariableInputs(dashboardPage, selectors, variable); + + // enable the auto option + await page.getByText('Auto option').click(); + + // select the variable in the dashboard and confirm the variable value is set + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem).click(); + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.label!) + ); + await expect(variableLabel).toBeVisible(); + await expect(variableLabel).toContainText(variable.label!); + + // assert the panel is visible and has the correct value + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content).first(); + await expect(panelContent).toBeVisible(); + const markdownContent = panelContent.locator('.markdown-html'); + await expect(markdownContent).toContainText(`VariableUnderTest: ${variable.value}`); + + // select the variable in the dashboard and set the Auto option + const variableDropdown = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels(variable.name!) + ); + const nextElement = variableDropdown.locator('+ *'); + await expect(nextElement).toHaveText('1m'); + await nextElement.click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Auto' }).click(); + + // assert the panel is visible and has the correct "Auto" value + await expect(panelContent).toBeVisible(); + await expect(markdownContent).toContainText('VariableUnderTest: 10m'); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts new file mode 100644 index 00000000000..b376c6f6af2 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-move-panel.spec.ts @@ -0,0 +1,120 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'ed155665/annotation-filtering'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, + // these tests require a larger viewport + viewport: { width: 1920, height: 1080 }, +}); + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can drag and drop panels', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Move panel three to panel one position + await movePanel(dashboardPage, selectors, /^Panel three$/, /^Panel one$/); + + // Verify panel three is now above panel one + const panel3Position = await getPanelPosition(dashboardPage, selectors, /^Panel three$/); + const panel1Position = await getPanelPosition(dashboardPage, selectors, /^Panel one$/); + + expect(panel3Position?.y).toBeLessThan(panel1Position?.y || 0); + + // Move panel two to panel three position + await movePanel(dashboardPage, selectors, /^Panel two$/, /^Panel three$/); + + // Verify panel two is now above panel three + const panel2Position = await getPanelPosition(dashboardPage, selectors, /^Panel two$/); + const panel3PositionAfter = await getPanelPosition(dashboardPage, selectors, /^Panel three$/); + + expect(panel2Position?.y).toBeLessThan(panel3PositionAfter?.y || 0); + }); + + // Note, moving a panel from a nested row to a parent row currently just deletes the panel + // This test will need to be updated once the correct behavior is implemented. + test('can move panel from nested row to parent row', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?orgId=1` }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + await groupIntoRow(page, dashboardPage, selectors); + + const firstRowElement = dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('New row')) + .first(); + const rowBoundingBox = await firstRowElement.boundingBox(); + + if (!rowBoundingBox) { + throw new Error('Row element not found'); + } + + const panel1Element = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel one$/ }); + + await panel1Element.hover(); + await page.mouse.down(); + await page.mouse.move(rowBoundingBox.x, rowBoundingBox.y); + await page.mouse.up(); + + await expect( + dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel one$/ }) + ).toBeHidden(); + }); + } +); + +// Helper functions +async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into row').click(); +} + +async function getPanelPosition( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + panelTitle: string | RegExp +) { + const panel = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: panelTitle }); + const boundingBox = await panel.boundingBox(); + return boundingBox; +} + +async function movePanel( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + sourcePanel: string | RegExp, + targetPanel: string | RegExp +) { + // Get target panel position + const targetPanelElement = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: targetPanel }); + + // Get source panel element + const sourcePanelElement = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: sourcePanel }); + + // Perform drag and drop + await sourcePanelElement.dragTo(targetPanelElement); +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts new file mode 100644 index 00000000000..2cdb847a03f --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -0,0 +1,409 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +test.describe( + 'Dashboard Panel Layouts', + { + tag: ['@dashboards'], + }, + () => { + test('can switch to auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Switch to auto grid'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await checkAutoGridLayoutInputs(dashboardPage, selectors); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await checkAutoGridLayoutInputs(dashboardPage, selectors); + }); + + test('can change min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set min column width'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + // Get initial positions - standard width should have panels on different rows + const firstPanelTop = await getPanelTop(dashboardPage, selectors); + const lastPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).last(); + const lastPanelBox = await lastPanel.boundingBox(); + const lastPanelTop = lastPanelBox?.y || 0; + + // Verify standard layout has panels on different rows + expect(lastPanelTop).toBeGreaterThan(firstPanelTop); + + // Change to narrow min column width + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + .click(); + await page.getByRole('option', { name: 'Narrow' }).click(); + + // Verify narrow layout has all panels on same row + const firstPanelTopNarrow = await getPanelTop(dashboardPage, selectors); + const lastPanelBoxNarrow = await lastPanel.boundingBox(); + const lastPanelTopNarrow = lastPanelBoxNarrow?.y || 0; + + expect(lastPanelTopNarrow).toBe(firstPanelTopNarrow); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth + ) + ).toHaveValue('Narrow'); + + const firstPanelTopReload = await getPanelTop(dashboardPage, selectors); + const lastPanelBoxReload = await lastPanel.boundingBox(); + const lastPanelTopReload = lastPanelBoxReload?.y || 0; + + expect(lastPanelTopReload).toBe(firstPanelTopReload); + }); + + test('can change to custom min column width in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set custom min column width'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + .click(); + await page.getByRole('option', { name: 'Custom' }).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) + .fill('900'); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth) + .blur(); + + // Changing to 900 custom width should have each panel span the whole row (stacked vertically) + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customMinColumnWidth + ) + ).toHaveValue('900'); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomMinColumnWidth) + .click(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth + ) + ).toHaveValue('Standard'); + }); + + test('can change max columns in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set max columns'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + + // Changing to 1 max column should have each panel span the whole row (stacked vertically) + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) + ).toHaveValue('1'); + + await verifyPanelsStackedVertically(dashboardPage, selectors); + }); + + test('can change row height in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set row height'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + const regularRowHeight = await getPanelHeight(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + .click(); + await page.getByRole('option', { name: 'Short' }).click(); + + const shortHeight = await getPanelHeight(dashboardPage, selectors); + expect(shortHeight).toBeLessThan(regularRowHeight); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + .click(); + await page.getByRole('option', { name: 'Tall' }).click(); + + const tallHeight = await getPanelHeight(dashboardPage, selectors); + expect(tallHeight).toBeGreaterThan(regularRowHeight); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + const tallHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(tallHeightAfterReload).toBeGreaterThan(regularRowHeight); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + ).toHaveValue('Tall'); + + const tallHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); + expect(tallHeightAfterEdit).toBeGreaterThan(regularRowHeight); + }); + + test('can change to custom row height in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set custom row height'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + const regularRowHeight = await getPanelHeight(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + .click(); + await page.getByRole('option', { name: 'Custom' }).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight) + .fill('800'); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight) + .blur(); + + const customHeight = await getPanelHeight(dashboardPage, selectors); + expect(customHeight).toBeCloseTo(800, 5); // Allow some tolerance for rendering differences + expect(customHeight).toBeGreaterThan(regularRowHeight); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + const customHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(customHeightAfterReload).toBeCloseTo(800, 5); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.customRowHeight + ) + ).toHaveValue('800'); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.clearCustomRowHeight) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + ).toHaveValue('Standard'); + }); + + test('can change fill screen in auto grid layout', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Set fill screen'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toHaveCount(3); + + await dashboardPage + .getByGrafanaSelector(selectors.components.OptionsGroup.toggle('grid-layout-category')) + .click(); + + await page.getByLabel('Auto grid').click(); + + // Set narrow column width first to ensure panels fit horizontally + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + .click(); + await page.getByRole('option', { name: 'Narrow' }).click(); + + const initialHeight = await getPanelHeight(dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) + .click({ force: true }); + + const fillScreenHeight = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeight).toBeGreaterThan(initialHeight); + + await saveDashboard(dashboardPage, selectors); + await page.reload(); + + const fillScreenHeightAfterReload = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeightAfterReload).toBeGreaterThan(initialHeight); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) + ).toBeChecked(); + + const fillScreenHeightAfterEdit = await getPanelHeight(dashboardPage, selectors); + expect(fillScreenHeightAfterEdit).toBeGreaterThan(initialHeight); + }); + } +); + +// Helper functions +async function importTestDashboard(page: Page, selectors: E2ESelectorGroups, title: string) { + await page.goto(selectors.pages.ImportDashboard.url); + await page.getByTestId(selectors.components.DashboardImportPage.textarea).fill(JSON.stringify(testV2Dashboard)); + await page.getByTestId(selectors.components.DashboardImportPage.submit).click(); + await page.getByTestId(selectors.components.ImportDashboardForm.name).fill(title); + await page.getByTestId(selectors.components.ImportDashboardForm.submit).click(); +} + +async function saveDashboard(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.saveButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.DashboardSaveDrawer.saveButton).click(); +} + +async function checkAutoGridLayoutInputs(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.rowHeight) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.fillScreen) + ).toBeVisible(); +} + +async function verifyPanelsStackedVertically(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + const panels = await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).all(); + let previousTop = 0; + + for (const panel of panels) { + const boundingBox = await panel.boundingBox(); + if (boundingBox) { + if (previousTop === 0) { + previousTop = boundingBox.y; + } else { + expect(boundingBox.y).toBeGreaterThan(previousTop); + previousTop = boundingBox.y; + } + } + } +} + +async function getPanelHeight(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first(); + const boundingBox = await panel.boundingBox(); + return boundingBox?.height || 0; +} + +async function getPanelTop(dashboardPage: DashboardPage, selectors: E2ESelectorGroups): Promise { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first(); + const boundingBox = await panel.boundingBox(); + return boundingBox?.y || 0; +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts new file mode 100644 index 00000000000..5b5202f1455 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-remove-panel.spec.ts @@ -0,0 +1,66 @@ +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +test.describe( + 'Dashboard panels', + { + tag: ['@dashboards'], + }, + () => { + test('can remove a panel', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Remove Panel #1 + await removePanelsByTitle(dashboardPage, selectors, ['Panel #1']); + + // Check that panel has been deleted + await expect( + dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel #1$/ }) + ).toBeHidden(); + }); + + test('can remove several panels at once', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Remove multiple panels + await removePanelsByTitle(dashboardPage, selectors, ['Panel #1', 'Panel #2', 'Panel #3']); + + // Check that panels have been deleted + await expect( + dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: /^Panel #[123]$/ }) + ).toBeHidden(); + }); + } +); + +// Helper function to remove a panel by its title +async function removePanelsByTitle(dashboardPage: DashboardPage, selectors: E2ESelectorGroups, panelTitles: string[]) { + for (const panelTitle of panelTitles) { + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: panelTitle }) + .click({ + modifiers: ['Shift'], + }); + } + + await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.deleteButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); +} diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts new file mode 100644 index 00000000000..87b8fc6ff3b --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-title-description.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +const PAGE_UNDER_TEST = 'ed155665/annotation-filtering'; + +test.describe( + 'Dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('can change dashboard description and title', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // Check that current dashboard title is visible in breadcrumb + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('Annotation filtering')) + ).toBeVisible(); + + const titleInput = page.locator('[aria-label="dashboard-options Title field property editor"] input'); + await expect(titleInput).toHaveValue('Annotation filtering'); + await titleInput.fill('New dashboard title'); + await expect(titleInput).toHaveValue('New dashboard title'); + + // Check that new dashboard title is reflected in breadcrumb + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb('New dashboard title')) + ).toBeVisible(); + + // Check that we can successfully change the dashboard description + const descriptionTextArea = page.locator( + '[aria-label="dashboard-options Description field property editor"] textarea' + ); + await descriptionTextArea.fill('Dashboard description'); + await expect(descriptionTextArea).toHaveValue('Dashboard description'); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts new file mode 100644 index 00000000000..1ef85b85261 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -0,0 +1,84 @@ +import { DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const deselectPanels = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls).click({ + position: { x: 0, y: 0 }, + }); +}; + +export const flows = { + deselectPanels, + async changePanelTitle( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + oldPanelTitle: string, + newPanelTitle: string + ) { + await deselectPanels(dashboardPage, selectors); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: oldPanelTitle }) + .first() + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldInput('Title')) + .fill(newPanelTitle); + }, + async changePanelDescription( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + panelTitle: string, + newDescription: string + ) { + await deselectPanels(dashboardPage, selectors); + const panelTitleRegex = new RegExp(`^${panelTitle}$`); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.headerContainer) + .filter({ hasText: panelTitleRegex }) + .first() + .click(); + const descriptionTextArea = dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('panel-options Description')) + .locator('textarea'); + await descriptionTextArea.fill(newDescription); + }, + async newEditPaneVariableClick(dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.section).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton) + .click(); + }, + async newEditPanelCommonVariableInputs( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + variable: Variable + ) { + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.variableType(variable.type)) + .click(); + const variableNameInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.variableNameInput + ); + await variableNameInput.click(); + await variableNameInput.fill(variable.name); + await variableNameInput.blur(); + if (variable.label) { + const variableLabelInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.variableLabelInput + ); + await variableLabelInput.click(); + await variableLabelInput.fill(variable.label); + await variableLabelInput.blur(); + } + }, +}; + +export type Variable = { + type: string; + name: string; + label?: string; + description?: string; + value: string; +}; diff --git a/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts b/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts new file mode 100644 index 00000000000..742aa106336 --- /dev/null +++ b/e2e-playwright/dashboards-search-suite/dashboards-search.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Dashboard search', + { + tag: ['@dashboards-search'], + }, + () => { + test.use({ viewport: { width: 1280, height: 1080 } }); + + test.beforeEach(async ({ page, selectors }) => { + await page.goto('/dashboards'); + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible(); + }); + + test('Search - Dashboards list', async ({ page, selectors }) => { + await toggleSearchView(page, selectors); + await assertResultsCount(page, 24); + }); + + test('Search - Filter by search input', async ({ page, selectors }) => { + await toggleSearchView(page, selectors); + await assertResultsCount(page, 24); + + const searchInput = await page.getByTestId('input-wrapper'); + await searchInput.click(); + await page.keyboard.type('Datasource tests - MySQL'); + + await assertResultsCount(page, 2); + + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + await page.keyboard.type('Datasource tests - MySQL (unittest)'); + + await assertResultsCount(page, 1); + + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + await page.keyboard.type('- MySQL'); + + await assertResultsCount(page, 2); + }); + } +); + +async function assertResultsCount(page, length) { + const rowGroup = await page.getByRole('rowgroup'); + await expect(rowGroup).toHaveCount(1); + + const rows = await rowGroup.first().getByRole('row'); + await expect(rows).toHaveCount(length); +} + +async function toggleSearchView(page, selectors) { + const toggleButtons = await page.getByTestId(selectors.pages.Dashboards.toggleView); + await expect(toggleButtons).toHaveCount(2); + + const listRadioButton = await toggleButtons.nth(1).locator('input'); + await expect(listRadioButton).toBeChecked({ checked: false }); + + await listRadioButton.check(); + await expect(listRadioButton).toBeChecked({ checked: true }); +} diff --git a/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts new file mode 100644 index 00000000000..7f88e462deb --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @@ -0,0 +1,126 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { makeNewDashboardRequestBody } from './utils/makeDashboard'; + +const NUM_ROOT_FOLDERS = 60; +const NUM_ROOT_DASHBOARDS = 60; +const NUM_NESTED_FOLDERS = 60; +const NUM_NESTED_DASHBOARDS = 60; + +// TODO change this test so it doesn't conflict with the existing dashboard browse test +// probably needs a separate user +test.describe.skip( + 'Dashboard browse (nested)', + { + tag: ['@dashboards'], + }, + () => { + const dashboardUIDsToCleanUp: string[] = []; + const folderUIDsToCleanUp: string[] = []; + + test.beforeAll(async ({ request }) => { + // Add root folders + for (let i = 0; i < NUM_ROOT_FOLDERS; i++) { + const response = await request.post('/api/folders', { + data: { + title: `Root folder ${i.toString().padStart(2, '0')}`, + }, + }); + const responseBody = await response.json(); + folderUIDsToCleanUp.push(responseBody.uid); + } + + // Add root dashboards + for (let i = 0; i < NUM_ROOT_DASHBOARDS; i++) { + const response = await request.post('/api/dashboards/db', { + data: makeNewDashboardRequestBody(`Root dashboard ${i.toString().padStart(2, '0')}`), + }); + const responseBody = await response.json(); + dashboardUIDsToCleanUp.push(responseBody.uid); + } + + // Add folder with children + const folderResponse = await request.post('/api/folders', { + data: { + title: 'A root folder with children', + }, + }); + const folderResponseBody = await folderResponse.json(); + const folderUid = folderResponseBody.uid; + folderUIDsToCleanUp.push(folderUid); + + // Add nested folders + for (let i = 0; i < NUM_NESTED_FOLDERS; i++) { + await request.post('/api/folders', { + data: { + title: `Nested folder ${i.toString().padStart(2, '0')}`, + parentUid: folderUid, + }, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + // Add nested dashboards + for (let i = 0; i < NUM_NESTED_DASHBOARDS; i++) { + await request.post('/api/dashboards/db', { + data: makeNewDashboardRequestBody(`Nested dashboard ${i.toString().padStart(2, '0')}`, folderUid), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + }); + + test.afterAll(async ({ request }) => { + // Clean up root dashboards + for (const dashboardUID of dashboardUIDsToCleanUp) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + + // Clean up root folders (cascading delete will remove any nested folders and dashboards) + for (const folderUID of folderUIDsToCleanUp) { + await request.delete(`/api/folders/${folderUID}`, { + params: { + forceDeleteRules: false, + }, + }); + } + }); + + test('pagination works correctly for folders and root', async ({ page, selectors }) => { + // Navigate to dashboards page + await page.goto('/dashboards'); + + // Wait for and verify the root folder with children is visible + await expect(page.getByText('A root folder with children')).toBeVisible(); + + // Expand A root folder with children + await page.getByLabel('Expand folder A root folder with children').click(); + await expect(page.getByText('Nested folder 00')).toBeVisible(); + + // Get the table body container for scrolling + const tableBody = page.getByTestId(selectors.pages.BrowseDashboards.table.body).locator('> div'); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 2100)); + await expect(page.getByText('Nested folder 59')).toBeVisible(); + await expect(page.getByText('Nested dashboard 00')).toBeVisible(); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 4200)); + await expect(page.getByText('Nested dashboard 59')).toBeVisible(); + await expect(page.getByText('Root folder 00')).toBeVisible(); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 6300)); + await expect(page.getByText('Root folder 59')).toBeVisible(); + await expect(page.getByText('Root dashboard 00')).toBeVisible(); + + // Scroll the page and check visibility of next set of items + await tableBody.evaluate((el) => el.scrollTo(0, 8400)); + await expect(page.getByText('Root dashboard 59')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts new file mode 100644 index 00000000000..98106773c2d --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-browse.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/TestDashboard.json'; + +test.describe( + 'Dashboard browse', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test.beforeAll(async ({ request }) => { + // Import the test dashboard + const response = await request.post('/api/dashboards/import', { + data: { + dashboard: testDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); + const responseBody = await response.json(); + dashboardUID = responseBody.uid; + }); + + test.afterAll(async ({ request }) => { + // Clean up the imported dashboard + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + }); + + test('Manage Dashboards tests', async ({ page, selectors }) => { + // Navigate to dashboards page + await page.goto('/dashboards'); + + // Folders and dashboards should be visible + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards'))).toBeVisible(); + await expect( + page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard')) + ).toBeVisible(); + + // gdev dashboards folder is collapsed - its content should not be visible + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeHidden(); + + // should click a folder and see its children + await page + .getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards')) + .getByLabel(/Expand folder/) + .click(); + await expect(page.getByTestId(selectors.pages.BrowseDashboards.table.row('Bar Gauge Demo'))).toBeVisible(); + + // Open the new folder drawer + await page.getByText('New').click(); + await page.getByRole('menuitem', { name: 'New folder' }).click(); + + // And create a new folder + await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.nameInput).fill('My new folder'); + await page + .getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form) + .getByRole('button', { name: 'Create' }) + .click(); + // await page.getByTestId(selectors.pages.BrowseDashboards.NewFolderForm.form).getByRole('button', { name: 'Create' }).click({ force: true }); + + // Verify success alert and close it + const alert = page.getByTestId(selectors.components.Alert.alertV2('success')); + await expect(alert).toBeVisible(); + await alert.getByLabel('Close alert').click(); + await expect(page.getByRole('heading', { name: 'My new folder' })).toBeVisible(); + + // Delete the folder and expect to go back to the root + await page.getByRole('button', { name: 'Folder actions' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete'); + await page.getByTestId(selectors.pages.ConfirmModal.delete).click(); + await expect(page.getByRole('heading', { name: 'Dashboards' })).toBeVisible(); + + // Can collapse the gdev folder and delete the dashboard we imported + await page + .getByTestId(selectors.pages.BrowseDashboards.table.row('gdev dashboards')) + .getByLabel(/Collapse folder/) + .click(); + + // Select the imported dashboard using checkbox + await page + .getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard')) + .getByRole('checkbox') + .click({ force: true }); + + // Delete the selected dashboard + await page.getByRole('button', { name: 'Delete' }).click(); + + // Confirm deletion in modal + await page.getByPlaceholder('Type "Delete" to confirm').fill('Delete'); + await page.getByTestId(selectors.pages.ConfirmModal.delete).click(); + await expect( + page.getByTestId(selectors.pages.BrowseDashboards.table.row('E2E Test - Import Dashboard')) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts new file mode 100644 index 00000000000..d5a3f4ecac3 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-export-json.spec.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Export as JSON', + { + tag: ['@dashboards'], + }, + () => { + test('Export for internal and external use', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'ZqZnVvFZz', + }); + + // Open the export drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.NewExportButton.Menu.exportAsJson) + .click(); + + await expect(page).toHaveURL(/.*shareView=export.*/); + + // Export as JSON + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.container) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle) + ).toBeChecked({ + checked: false, + }); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.CodeEditor.container)).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.saveToFileButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton) + .click(); + // TODO failing in CI - fix it + // let clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); + // expect(clipboardContent).not.toContain('__inputs'); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.exportExternallyToggle) + .click({ force: true }); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.copyToClipboardButton) + .click(); + // TODO failing in CI - fix it + // clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); + // expect(clipboardContent).toContain('__inputs'); + + await dashboardPage.getByGrafanaSelector(selectors.pages.ExportDashboardDrawer.ExportAsJson.cancelButton).click(); + + await expect(page).not.toHaveURL(/.*shareView=export.*/); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts new file mode 100644 index 00000000000..893afe13f09 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-keybindings.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Dashboard keybindings', + { + tag: ['@dashboards'], + }, + () => { + // the "should collapse and expand all rows" test requires a larger viewport + // otherwise the final panel is not visible when everything is expanded + test.use({ + viewport: { width: 1280, height: 1080 }, + }); + + test('should collapse and expand all rows', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'Repeating-rows-uid/repeating-rows' }); + + const panelContents = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content); + await expect(panelContents).toHaveCount(5); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = A, pod = Bob')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = B, pod = Bob')) + ).toBeVisible(); + + // Collapse all rows using keyboard shortcut: d + Shift+C + await page.keyboard.press('d'); + await page.keyboard.press('Shift+C'); + + await expect(panelContents).toHaveCount(0); + await expect(page.getByText('server = A, pod = Bob')).toBeHidden(); + await expect(page.getByText('server = B, pod = Bob')).toBeHidden(); + + // Expand all rows using keyboard shortcut: d + Shift+E + await page.keyboard.press('d'); + await page.keyboard.press('Shift+E'); + + await expect(panelContents).toHaveCount(6); + await expect(page.getByText('server = A, pod = Bob')).toBeVisible(); + await expect(page.getByText('server = B, pod = Bob')).toBeVisible(); + }); + + test('should open panel inspect', async ({ gotoDashboardPage, page, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'edediimbjhdz4b/a-tall-dashboard' }); + + // Find Panel #1 and press 'i' to open inspector + const panel1 = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1')); + await expect(panel1).toBeVisible(); + await panel1.press('i'); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible(); + + // Press Escape to close inspector + await page.keyboard.press('Escape'); + + await expect(page.getByTestId(selectors.components.PanelInspector.Json.content)).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts new file mode 100644 index 00000000000..89a03671f5a --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-links-without-slug.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json'; + +test.describe( + 'Dashboard with data links that have no slug', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test.beforeAll(async ({ request }) => { + // Import the test dashboard + const response = await request.post('/api/dashboards/import', { + data: { + dashboard: testDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); + const responseBody = await response.json(); + dashboardUID = responseBody.uid; + }); + + test.afterAll(async ({ request }) => { + // Clean up the imported dashboard + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + }); + + test('Should not reload if linking to same dashboard', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: dashboardUID }); + + const panel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Data links without slug') + ); + await expect(panel).toBeVisible(); + + const urlShouldContain = '/d/data-link-no-slug/data-link-without-slug-test'; + await dashboardPage + .getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink) + .getByText(/9yy21uzzxypg/) + .click(); + await expect(page.getByText(/Loading/)).toBeHidden(); + await expect(page).toHaveURL(new RegExp(urlShouldContain)); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink) + .getByText(/dr199bpvpcru/) + .click(); + await expect(page.getByText(/Loading/)).toBeHidden(); + await expect(page).toHaveURL(new RegExp(urlShouldContain)); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DataLinksContextMenu.singleLink) + .getByText(/dre33fzyxcrz/) + .click(); + await expect(page.getByText(/Loading/)).toBeHidden(); + await expect(page).toHaveURL(new RegExp(urlShouldContain)); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts new file mode 100644 index 00000000000..79ae94c5e96 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-live-streaming.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/DashboardLiveTest.json'; + +test.describe( + 'Dashboard Live streaming support', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test.beforeAll(async ({ request }) => { + // Import the test dashboard + const response = await request.post('/api/dashboards/import', { + data: { + dashboard: testDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); + const responseBody = await response.json(); + dashboardUID = responseBody.uid; + }); + + test.afterAll(async ({ request }) => { + // Clean up the imported dashboard + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + } + }); + + test('Should receive streaming data', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: dashboardUID }); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Live'))).toBeVisible(); + await expect + .poll( + async () => + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body) + .getByRole('row') + .count() + ) + .toBeGreaterThan(5); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts new file mode 100644 index 00000000000..3263b4f3011 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-public-create.spec.ts @@ -0,0 +1,211 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + newDashboardSharingComponent: false, + }, +}); + +test.describe( + 'Public dashboards', + { + tag: ['@dashboards'], + }, + () => { + test('Create, open and disable a public dashboard', async ({ page, gotoDashboardPage, selectors, request }) => { + // Navigate to dashboard without template variables + let dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' }); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + // Create button should be disabled + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeDisabled(); + + // Create flow shouldn't show these elements + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton) + ).toBeHidden(); + + // Acknowledge checkboxes + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox) + .click({ force: true }); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox) + .click({ force: true }); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox) + .click({ force: true }); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeEnabled(); + + // Create public dashboard + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + .click(); + + // These elements shouldn't be rendered after creating public dashboard + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeHidden(); + + // These elements should be rendered + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + .click(); + + // These elements should be rendered once the Settings dropdown is opened + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch) + ).toBeVisible(); + + // Close the sharing modal + await page.getByRole('button', { name: 'Close', exact: true }).click(); + + // Tag indicating a dashboard is public + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.publicDashboardTag) + ).toBeVisible(); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.DeleteButton) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.SettingsDropdown) + .click(); + + // These elements should be rendered once the Settings dropdown is opened + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch) + ).toBeVisible(); + + // Make a request to public dashboards api endpoint without authentication + let copyUrlInput = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput + ); + let url = await copyUrlInput.inputValue(); + let publicDashboardApiUrl = getPublicDashboardAPIUrl(url); + + // Create a new context without authentication + let response = await request.get(publicDashboardApiUrl); + expect(response.status()).toBe(200); + + // Close the sharing modal + await page.getByRole('button', { name: 'Close', exact: true }).click(); + + // Navigate to dashboard without template variables + dashboardPage = await gotoDashboardPage({ uid: 'ZqZnVvFZz' }); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + // Save url before disabling public dashboard + copyUrlInput = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput + ); + url = await copyUrlInput.inputValue(); + + // Switch off enabling toggle + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeEnabled(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + .click({ force: true }); + + // Url should be disabled + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput) + ).toBeDisabled(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton) + ).toBeDisabled(); + + // Make a request to public dashboards api endpoint without authentication + publicDashboardApiUrl = getPublicDashboardAPIUrl(url); + response = await request.get(publicDashboardApiUrl); + expect(response.status()).toBe(403); + }); + } +); + +const getPublicDashboardAPIUrl = (url: string): string => { + const accessToken = url.split('/').pop(); + return `/api/public/dashboards/${accessToken}`; +}; diff --git a/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts new file mode 100644 index 00000000000..60b928c1301 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-public-templating.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + newDashboardSharingComponent: false, + }, +}); + +test.describe( + 'Public dashboard with template variables', + { + tag: ['@dashboards'], + }, + () => { + test('Create a public dashboard with template variables shows a template variable warning', async ({ + gotoDashboardPage, + selectors, + }) => { + // Navigate to dashboard with template variables + const dashboardPage = await gotoDashboardPage({ + uid: 'HYaGDGIMk', + }); + + // Open sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select public dashboards tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Public Dashboard')).click(); + + // Warning Alert dashboard cannot be made public because it has template variables + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert + ) + ).toBeVisible(); + + // Configuration elements for public dashboards should exist + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.CreateButton) + ).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.PublicDashboard.PauseSwitch) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts new file mode 100644 index 00000000000..0d991c100e1 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -0,0 +1,191 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + }, +}); + +const DASHBOARD_UID = 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8'; +const DASHBOARD_UID_2 = 'edediimbjhdz4b'; + +test.describe( + 'Shared dashboards', + { + tag: ['@dashboards'], + }, + () => { + test('Close share externally drawer', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + // Open share externally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally) + .click(); + + await expect(page).toHaveURL(/.*shareView=public_dashboard.*/); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.cancelButton) + .click(); + + await expect(page).not.toHaveURL(/.*shareView=public_dashboard.*/); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.container) + ).toBeHidden(); + }); + + test('Create and disable a shared dashboard and check API', async ({ + page, + gotoDashboardPage, + selectors, + request, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID_2 }); + + // Open share externally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareExternally) + .click(); + + // Create button should be disabled + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton + ) + ).toBeDisabled(); + + // Create flow shouldn't show these elements + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton + ) + ).toBeHidden(); + + // Acknowledge checkbox + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox + ) + ).toBeEnabled(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox) + .click({ force: true }); + + // Create shared dashboard + const createResponse = page.waitForResponse( + (response) => + response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards`) && + response.request().method() === 'POST' + ); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton + ) + ).toBeEnabled(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton) + .click(); + + let response = await createResponse; + let publicDashboard = await response.json(); + + // Test API access with the created dashboard + let apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`); + expect(apiResponse.status()).toBe(200); + + // These elements shouldn't be rendered after creating public dashboard + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.willBePublicCheckbox + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Creation.PublicShare.createButton + ) + ).toBeHidden(); + + // These elements should be rendered + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableTimeRangeSwitch + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.enableAnnotationsSwitch + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.copyUrlButton + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.revokeAccessButton + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton + ) + ).toBeVisible(); + + // Switch off enabling toggle + const updateResponse = page.waitForResponse( + (response) => + response.url().includes(`/api/dashboards/uid/${DASHBOARD_UID_2}/public-dashboards/`) && + response.request().method() === 'PATCH' + ); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton + ) + ).toBeEnabled(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareExternally.Configuration.toggleAccessButton) + .click({ force: true }); + + response = await updateResponse; + expect(response.status()).toBe(200); + + publicDashboard = await response.json(); + + // Test that API access is now forbidden + apiResponse = await request.get(`/api/public/dashboards/${publicDashboard.accessToken}`); + expect(apiResponse.status()).toBe(403); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts new file mode 100644 index 00000000000..d250bb73464 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-internally.spec.ts @@ -0,0 +1,175 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + }, +}); + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.describe( + 'Share internally', + { + tag: ['@dashboards'], + }, + () => { + test.beforeEach(async ({ page }) => { + // Clear localStorage before each test + await page.evaluate(() => { + window.localStorage.removeItem('grafana.dashboard.link.shareConfiguration'); + }); + }); + + test('Create a locked time range short link', async ({ page, gotoDashboardPage, selectors }) => { + // Navigate to dashboard with specific time range + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }), + }); + + // Open share internally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + + const createResponse = page.waitForResponse( + (response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST' + ); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally) + .click(); + + await expect(page).toHaveURL(/.*shareView=link.*/); + + // Check that the required elements exist + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareInternally.copyUrlButton) + ).toBeVisible(); + + // Check radio buttons + const radioButtons = dashboardPage.getByGrafanaSelector(selectors.components.RadioButton.container); + await expect(radioButtons).toHaveCount(3); + + // Check localStorage is initially null + const initialConfig = await page.evaluate(() => { + return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + }); + expect(initialConfig).toBeNull(); + + // Wait for the short URL creation + const response = await createResponse; + expect(response.status()).toBe(200); + + const responseBody = await response.json(); + expect(responseBody.url).toContain('goto'); + }); + + test('Create a relative time range short link', async ({ page, gotoDashboardPage, selectors }) => { + // Navigate to dashboard with specific time range + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }), + }); + + // Open share internally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally) + .click(); + + const updateResponse = page.waitForResponse( + (response) => response.url().includes('/api/short-urls') && response.request().method() === 'POST' + ); + + // Toggle the lock time range switch + const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch + ); + await expect(lockTimeRangeSwitch).toBeInViewport(); + await expect(async () => { + await lockTimeRangeSwitch.uncheck({ force: true }); + }).toPass(); + + await expect(async () => { + // Check localStorage configuration + const shareConfig = await page.evaluate(() => { + const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + return config ? JSON.parse(config) : null; + }); + + expect(shareConfig).not.toBeNull(); + expect(shareConfig.useAbsoluteTimeRange).toBe(false); + expect(shareConfig.useShortUrl).toBe(true); + expect(shareConfig.theme).toBe('current'); + }).toPass(); + + // Wait for the API response + const response = await updateResponse; + expect(response.status()).toBe(200); + + const responseBody = await response.json(); + expect(responseBody.url).toContain('goto'); + }); + + test('Share button gets configured link', async ({ page, gotoDashboardPage, selectors }) => { + // Navigate to dashboard with specific time range + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ from: 'now-6h', to: 'now' }), + }); + + // Open share internally drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareInternally) + .click(); + + // Check localStorage is initially null + const initialConfig = await page.evaluate(() => { + return window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + }); + expect(initialConfig).toBeNull(); + + // Configure the sharing options + const lockTimeRangeSwitch = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareInternally.lockTimeRangeSwitch + ); + await expect(lockTimeRangeSwitch).toBeInViewport(); + await expect(async () => { + await lockTimeRangeSwitch.uncheck({ force: true }); + }).toPass(); + const shortenUrlSwitch = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardDrawer.ShareInternally.shortenUrlSwitch + ); + await expect(shortenUrlSwitch).toBeInViewport(); + await expect(async () => { + await shortenUrlSwitch.uncheck({ force: true }); + }).toPass(); + + // Close the drawer + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click(); + + await expect(page).not.toHaveURL(/.*shareView=link.*/); + + await expect(async () => { + // Check that localStorage has been updated with the configuration + const finalConfig = await page.evaluate(() => { + const config = window.localStorage.getItem('grafana.dashboard.link.shareConfiguration'); + return config ? JSON.parse(config) : null; + }); + + expect(finalConfig).not.toBeNull(); + expect(finalConfig.useAbsoluteTimeRange).toBe(false); + expect(finalConfig.useShortUrl).toBe(false); + expect(finalConfig.theme).toBe('current'); + }).toPass(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts new file mode 100644 index 00000000000..a3390122e92 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { SnapshotCreateResponse } from '../../public/app/features/dashboard/services/SnapshotSrv'; + +test.use({ + featureToggles: { + scenes: true, + newDashboardSharingComponent: true, + }, +}); + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.describe( + 'Snapshots', + { + tag: ['@dashboards'], + }, + () => { + test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => { + // Opening a dashboard + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + const panelsToCheck = [ + 'Raw Data Graph', + 'Last non-null', + 'min', + 'Max', + 'The data from graph above with seriesToColumns transform', + ]; + + // Open the sharing drawer + await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.arrowMenu).click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.DashNav.newShareButton.menu.shareSnapshot) + .click(); + + // Publish snapshot + const createSnapshotPromise = page.waitForResponse( + (response) => response.url().includes('/api/snapshots') && response.request().method() === 'POST' + ); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardDrawer.ShareSnapshot.publishSnapshot) + .click(); + + const createResponse = await createSnapshotPromise; + expect(createResponse.status()).toBe(200); + + const responseBody: SnapshotCreateResponse = await createResponse.json(); + + // Navigate to the snapshot URL + const snapshotUrl = getSnapshotUrl(responseBody.key); + await page.goto(snapshotUrl); + + // Validate the dashboard controls are rendered + await expect(dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls)).toBeVisible(); + + // Validate the panels are rendered + for (const title of panelsToCheck) { + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title))).toBeVisible(); + } + }); + } +); + +const getSnapshotUrl = (uid: string): string => { + return `/dashboard/snapshot/${uid}`; +}; diff --git a/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts new file mode 100644 index 00000000000..b31fecf230f --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-templating.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'HYaGDGIMk'; + +test.use({ + timezoneId: 'Pacific/Easter', +}); + +test.describe( + 'Dashboard templating', + { + tag: ['@dashboards'], + }, + () => { + test('Verify variable interpolation works', async ({ page, gotoDashboardPage }) => { + // Open dashboard global variables and interpolation + await gotoDashboardPage({ uid: DASHBOARD_UID }); + + // Get the actual timezone from the browser context (should be Pacific/Easter due to test.use) + const timeZone = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone); + const example = `Example: from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}`; + + const expectedItems: string[] = [ + '__dashboard = Templating - Global variables and interpolation', + '__dashboard.name = Templating - Global variables and interpolation', + '__dashboard.uid = HYaGDGIMk', + '__org.name = Main Org.', + '__org.id = 1', + '__user.id = 1', + '__user.login = admin', + '__user.email = admin@localhost', + `Server:raw = A'A"A,BB\\B,CCC`, + `Server:regex = (A'A"A|BB\\\\B|CCC)`, + `Server:lucene = ("A'A\\"A" OR "BB\\\\B" OR "CCC")`, + `Server:glob = {A'A"A,BB\\B,CCC}`, + `Server:pipe = A'A"A|BB\\B|CCC`, + `Server:distributed = A'A"A,Server=BB\\B,Server=CCC`, + `Server:csv = A'A"A,BB\\B,CCC`, + `Server:html = A'A"A, BB\\B, CCC`, + `Server:json = ["A'A\\"A","BB\\\\B","CCC"]`, + `Server:percentencode = %7BA%27A%22A%2CBB%5CB%2CCCC%7D`, + `Server:singlequote = 'A\\'A"A','BB\\B','CCC'`, + `Server:doublequote = "A'A\\"A","BB\\B","CCC"`, + `Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`, + `Server:date = NaN`, + `Server:text = All`, + `Server:queryparam = var-Server=$__all`, + `1 < 2`, + example, + ]; + + // Get all list items from the markdown content + const listItems = page.locator('.markdown-html li'); + + // Verify we have the expected number of items + await expect(listItems).toHaveCount(26); + + // Get all the text content from list items + const actualItems = await listItems.allTextContents(); + + // Compare each expected item with actual items + for (let i = 0; i < expectedItems.length; i++) { + expect(actualItems[i]).toBe(expectedItems[i]); + } + + // Check link interpolation is working correctly + const exampleLink = page.locator(`a:has-text("${example}")`); + await expect(exampleLink).toHaveAttribute( + 'href', + `https://example.com/?from=now-6h&to=now&timezone=${encodeURIComponent(timeZone)}` + ); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts new file mode 100644 index 00000000000..ac505aa9cd4 --- /dev/null +++ b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts @@ -0,0 +1,97 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = '5SdHCasdf'; + +const USER = 'dashboard-timepicker-test'; +const PASSWORD = 'dashboard-timepicker-test'; + +// Separate user to isolate changes from other tests +test.use({ + user: { + user: USER, + password: PASSWORD, + }, + storageState: { + cookies: [], + origins: [], + }, +}); + +test.describe( + 'Dashboard timepicker', + { + tag: ['@dashboards'], + }, + () => { + test('Shows the correct calendar days with custom timezone set via preferences', async ({ + createUser, + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await createUser(); + // login manually for now + await page.getByTestId(selectors.pages.Login.username).fill(USER); + await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD); + await page.getByTestId(selectors.pages.Login.submit).click(); + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + + // Set user preferences for timezone + await page.goto('/profile'); + await page.getByTestId(selectors.components.TimeZonePicker.containerV2).click(); + await page.getByRole('option', { name: 'Asia/Tokyo' }).click(); + await page.getByTestId(selectors.components.UserProfile.preferencesSaveButton).click(); + + // Open dashboard with time range from 8th to end of 10th. + // Will be Tokyo time because of above preference + await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ + timezone: 'Default', + }), + }); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click(); + + // Assert that the calendar shows 08 and 09 and 10 as selected days + const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive'); + await expect(activeTiles).toHaveCount(3); + }); + + test('Shows the correct calendar days with custom timezone set via time picker', async ({ + createUser, + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await createUser(); + // login manually for now + await page.getByTestId(selectors.pages.Login.username).fill(USER); + await page.getByTestId(selectors.pages.Login.password).fill(PASSWORD); + await page.getByTestId(selectors.pages.Login.submit).click(); + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + + // Open dashboard with time range from 2022-06-08 00:00:00 to 2022-06-10 23:59:59 in Tokyo time + await gotoDashboardPage({ + uid: DASHBOARD_UID, + queryParams: new URLSearchParams({ + timezone: 'Asia/Tokyo', + }), + }); + + // Open dashboard with time range from 8th to end of 10th. + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField).fill('2022-06-08 00:00:00'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField).fill('2022-06-10 23:59:59'); + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.calendar.openButton).first().click(); + + // Assert that the calendar shows 08 and 09 and 10 as selected days + const activeTiles = page.locator('.react-calendar__tile--active, .react-calendar__tile--hasActive'); + await expect(activeTiles).toHaveCount(3); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts new file mode 100644 index 00000000000..b6479d8007c --- /dev/null +++ b/e2e-playwright/dashboards-suite/embedded-dashboard.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Embedded dashboard', + { + tag: ['@dashboards'], + }, + () => { + test('open test page', async ({ page, dashboardPage, selectors }) => { + await page.goto('/dashboards/embedding-test'); + + // Verify pie charts are rendered + const pieChartSlices = page.locator( + `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` + ); + await expect(pieChartSlices).toHaveCount(5); + + // Verify no url sync + await dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton).click(); + await page.locator('label').filter({ hasText: 'Last 1 hour' }).click(); + + // Verify URL remains the same (no sync) + expect(page.url()).toMatch(/\/dashboards\/embedding-test$/); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/general-dashboards.spec.ts b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts new file mode 100644 index 00000000000..0a1978f3d69 --- /dev/null +++ b/e2e-playwright/dashboards-suite/general-dashboards.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'edediimbjhdz4b/a-tall-dashboard'; + +test.describe( + 'Dashboards', + { + tag: ['@dashboards'], + }, + () => { + test('should restore scroll position', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + // Verify first panel is visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1')) + ).toBeVisible(); + + // Scroll to the bottom + await page.evaluate(() => { + window.scrollTo(0, document.body.scrollHeight); + }); + + // The last panel should be visible... + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50')) + ).toBeVisible(); + + // Then we open and close the panel editor + // Click on panel menu (it only shows on hover) + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.menu('Panel #50')) + .click({ force: true }); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click(); + + // Go back to dashboard + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // The last panel should still be visible! + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #50')) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/import-dashboard.spec.ts b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts new file mode 100644 index 00000000000..cd905ac56b8 --- /dev/null +++ b/e2e-playwright/dashboards-suite/import-dashboard.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import testDashboard from '../dashboards/TestDashboard.json'; + +test.describe( + 'Import Dashboards Test', + { + tag: ['@dashboards'], + }, + () => { + let dashboardUID: string; + + test('Ensure you can import a number of json test dashboards from a specific test directory', async ({ + page, + dashboardPage, + selectors, + }) => { + await page.goto('/dashboard/import'); + + // Fill in the dashboard JSON and name + const textarea = dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.textarea); + await textarea.fill(JSON.stringify(testDashboard)); + + // Submit the JSON + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardImportPage.submit).click(); + + // Fill in the dashboard name and submit + const nameField = dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.name); + await expect(nameField).toBeVisible(); + await nameField.click(); + await nameField.clear(); + await nameField.fill(testDashboard.title); + + await dashboardPage.getByGrafanaSelector(selectors.components.ImportDashboardForm.submit).click(); + + // Wait for dashboard to load and extract UID from URL + await page.waitForURL('**/d/**'); + const url = page.url(); + const urlParts = url.split('/d/'); + if (urlParts.length > 1) { + dashboardUID = urlParts[1].split('/')[0]; + } + + // Verify the dashboard title is present in the breadcrumbs + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Breadcrumbs.breadcrumb(testDashboard.title)) + ).toBeVisible(); + + // Verify that specific panels from the test dashboard are loaded + await expect(page.getByText('Gauge Example')).toBeVisible(); + await expect(page.getByText('Stat')).toBeVisible(); + await expect(page.getByText('Time series example')).toBeVisible(); + }); + + test.afterEach(async ({ request }) => { + // Clean up the imported dashboard using API + if (dashboardUID) { + await request.delete(`/api/dashboards/uid/${dashboardUID}`); + dashboardUID = ''; + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts new file mode 100644 index 00000000000..ce1293acc2c --- /dev/null +++ b/e2e-playwright/dashboards-suite/load-options-from-url.spec.ts @@ -0,0 +1,300 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; + +test.describe( + 'Variables - Load options from Url', + { + tag: ['@dashboards'], + }, + () => { + test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + // Test first variable (A) + const firstVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('input'); + await expect(firstVariableInput).toBeVisible(); + await firstVariableInput.click(); + + // Check dropdown has 10 options + const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(firstDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test second variable (AA) + const secondVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')) + .locator('input'); + + await expect(secondVariableInput).toBeVisible(); + await secondVariableInput.click(); + + // Check dropdown has 10 options + const secondDropdownOptions = dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .locator('..'); + await expect(secondDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test third variable ($__all) + const thirdVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')) + .locator('input'); + + await expect(thirdVariableInput).toBeVisible(); + await thirdVariableInput.click(); + + // Check dropdown has 10 options + const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(thirdDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAC')) + ).toBeVisible(); + }); + + test('options set in url should load correct options', async ({ + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'B', + 'var-server': 'BB', + 'var-pod': 'BBB', + }), + }); + + // Test first variable (B) - should be selected from URL + const firstVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')) + .locator('input'); + + await expect(firstVariableInput).toBeVisible(); + await firstVariableInput.click(); + + // Check dropdown has 10 options + const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(firstDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test second variable (BB) - should be selected from URL + const secondVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')) + .locator('input'); + + await expect(secondVariableInput).toBeVisible(); + await secondVariableInput.click(); + + // Check dropdown has 10 options + const secondDropdownOptions = dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .locator('..'); + await expect(secondDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Test third variable (BBB) - should be selected from URL + const thirdVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')) + .locator('input'); + + await expect(thirdVariableInput).toBeVisible(); + await thirdVariableInput.click(); + + // Check dropdown has 10 options + const thirdDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(thirdDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC')) + ).toBeVisible(); + }); + + test('options set in url that do not exist should load correct options', async ({ + page, + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + // Handle uncaught exceptions that are expected + page.on('pageerror', (error) => { + if (error.message.includes("Couldn't find any field of type string in the results.")) { + // This error is expected and should not fail the test + return; + } + throw error; + }); + + await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'X', + }), + }); + + // Test first variable (X) - invalid value from URL + const firstVariableInput = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X')) + .locator('input'); + + await expect(firstVariableInput).toBeVisible(); + await firstVariableInput.click(); + + // Check dropdown has 10 options + const firstDropdownOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(firstDropdownOptions).toHaveCount(10); + + // Check toggle shows 'Selected (1)' + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + // Check specific options are visible (should fall back to default options) + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')) + ).toBeVisible(); + + // Close dropdown by clicking outside + await page.locator('body').click(); + + // Check that second variable shows $__all (should be 2 instances) + const allVariables = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + ); + + await expect(allVariables).toHaveCount(2); + await expect(allVariables.first()).toBeVisible(); + await expect(allVariables.last()).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts new file mode 100644 index 00000000000..99fa52b3a5c --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-constant-variable.spec.ts @@ -0,0 +1,65 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Variables - Constant', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new constant variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Constant" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + + // Select "Constant" type from dropdown + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Constant'); + await typeSelect.locator('input').press('Enter'); + + // Set variable name + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + + // Set constant value + const constantInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2 + ); + await constantInput.fill('pesto'); + + // Set label + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click(); + + // Assert the variable was rendered in the markdown content + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: pesto'); + + // Assert the variable is not visible in the dashboard navigation (constant variables are hidden) + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test')) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts new file mode 100644 index 00000000000..30a16a7dae0 --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-custom-variable.spec.ts @@ -0,0 +1,121 @@ +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +async function fillInCustomVariable( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + name: string, + label: string, + value: string +) { + // Select "Custom" type from dropdown + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Custom'); + await typeSelect.locator('input').press('Enter'); + + // Set variable name + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill(name); + + // Set label + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill(label); + + // Set custom values + const customValueInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput + ); + await customValueInput.fill(value); + await customValueInput.blur(); +} + +async function assertPreviewValues( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + expectedValues: string[] +) { + for (let i = 0; i < expectedValues.length; i++) { + const previewOption = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption) + .nth(i); + await expect(previewOption).toHaveText(expectedValues[i]); + } +} + +test.describe( + 'Variables - Custom', + { + tag: ['@dashboards'], + }, + () => { + test('can add a custom template variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Custom" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + await fillInCustomVariable(dashboardPage, selectors, 'VariableUnderTest', 'Variable under test', 'one,two,three'); + await assertPreviewValues(dashboardPage, selectors, ['one', 'two', 'three']); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one')) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'two' }).click(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: two'); + }); + + test('can add a custom template variable with labels', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Custom" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + await fillInCustomVariable( + dashboardPage, + selectors, + 'VariableUnderTest', + 'Variable under test', + 'One : 1,Two : 2, Three : 3' + ); + await assertPreviewValues(dashboardPage, selectors, ['One', 'Two', 'Three']); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1')) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: 'Two' }).click(); + + // Assert it was rendered (the value "2" should be displayed, not the label "Two") + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 2'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts new file mode 100644 index 00000000000..a769f8438cb --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-datasource-variable.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Variables - Datasource', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new datasource variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Datasource" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Data source'); + await typeSelect.locator('input').press('Enter'); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + + // If this is failing, make sure there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus" + // Or update to match available gdev datasources for testing + const datasourceSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect + ); + await datasourceSelect.locator('input').fill('Prometheus'); + await datasourceSelect.locator('input').press('Enter'); + + const previewOptions = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ); + await expect(previewOptions.first()).toContainText('gdev-prometheus'); + await expect(previewOptions.last()).toContainText('gdev-slow-prometheus'); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click(); + + // Change the selected variable value + await dashboardPage + .getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus') + ) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Select.option) + .filter({ hasText: 'gdev-slow-prometheus' }) + .click(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: gdev-slow-prometheus-uid'); + await expect(page.locator('.markdown-html').nth(1)).toContainText('VariableUnderTestText: gdev-slow-prometheus'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts new file mode 100644 index 00000000000..dbb670c739e --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-interval-variable.spec.ts @@ -0,0 +1,84 @@ +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +async function assertPreviewValues( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + expectedValues: string[] +) { + for (let i = 0; i < expectedValues.length; i++) { + const previewOption = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption) + .nth(i); + await expect(previewOption).toHaveText(expectedValues[i]); + } +} + +test.describe( + 'Variables - Interval', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new interval variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "Interval" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Interval'); + await typeSelect.locator('input').press('Enter'); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + + // Set interval values + const intervalInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput + ); + await intervalInput.fill('10s,10m,60m,90m,1h30m'); + await intervalInput.blur(); + await assertPreviewValues(dashboardPage, selectors, ['10s', '10m', '60m', '90m', '1h30m']); + + // Navigate back to the homepage and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2).click(); + + // Verify the variable label and initial value, then change the selected value + const variableLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test') + ); + + // Find the value element (next sibling) and verify it shows the default value + const variableValue = variableLabel.locator('+ div'); + await expect(variableValue).toHaveText('10s'); + + // Click to open the dropdown + await variableValue.click(); + + // Select '1h30m' from the dropdown + await dashboardPage.getByGrafanaSelector(selectors.components.Select.option).filter({ hasText: '1h30m' }).click(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: 1h30m'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-query-variable.spec.ts b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts new file mode 100644 index 00000000000..d289142a097 --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-query-variable.spec.ts @@ -0,0 +1,251 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; +const DASHBOARD_NAME = 'Templating - Nested Template Variables'; + +test.describe( + 'Variables - Query - Add variable', + { + tag: ['@dashboards'], + }, + () => { + test('query variable should be default and default fields should be correct', async ({ + page, + gotoDashboardPage, + selectors, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await expect(nameInput).toBeVisible(); + await expect(nameInput).toHaveAttribute('placeholder', 'Variable name'); + await expect(nameInput).toHaveValue('query0'); + + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await expect(typeSelect).toBeVisible(); + const singleValue = typeSelect.locator('div[class*="-singleValue"]'); + await expect(singleValue).toHaveText('Query'); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await expect(labelInput).toBeVisible(); + await expect(labelInput).toHaveAttribute('placeholder', 'Label name'); + await expect(labelInput).toHaveValue(''); + + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await expect(descriptionInput).toBeVisible(); + await expect(descriptionInput).toHaveAttribute('placeholder', 'Descriptive text'); + await expect(descriptionInput).toHaveValue(''); + + await expect(page.locator('label').filter({ hasText: 'Hide' })).toBeVisible(); + + // Check datasource selector + const datasourceSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect + ); + await expect(datasourceSelect).toBeVisible(); + await expect(datasourceSelect).toHaveAttribute('placeholder', 'gdev-testdata'); + + await expect(page.locator('label').filter({ hasText: 'Refresh' })).toBeVisible(); + await expect(page.locator('label').filter({ hasText: 'On dashboard load' })).toBeVisible(); + + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await expect(regexInput).toBeVisible(); + await expect(regexInput).toHaveAttribute('placeholder', '/.*-(?.*)-(?.*)-.*/'); + await expect(regexInput).toHaveValue(''); + + const sortSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2 + ); + await expect(sortSelect).toBeVisible(); + const sortSingleValue = sortSelect.locator('div[class*="-singleValue"]'); + await expect(sortSingleValue).toHaveText('Disabled'); + + // Check Multi-value checkbox + const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' }); + const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]'); + await expect(multiValueCheckbox).toBeChecked({ + checked: false, + }); + + // Check Include All option checkbox + const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' }); + const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]'); + await expect(includeAllCheckbox).toBeChecked({ + checked: false, + }); + + // Check preview and custom all input don't exist initially + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput + ) + ).toBeHidden(); + }); + + test('adding a single value query variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Click the "New" button to add a variable + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('a label'); + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await descriptionInput.fill('a description'); + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill('gdev-testdata'); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-testdata').click(); + const queryInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput + ); + await queryInput.fill('*'); + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await regexInput.fill('/.*C.*/'); + await regexInput.blur(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toBeVisible(); + + // Navigate back to dashboard + const submitButton = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton + ); + await submitButton.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // Check variable appears in submenu + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label')) + ).toBeVisible(); + + const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItems).toHaveCount(4); + const fourthItem = submenuItems.nth(3); + const input = fourthItem.locator('input'); + await input.click(); + + // Check dropdown has 1 option containing 'C' + const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(options).toHaveCount(1); + await expect(options).toContainText('C'); + }); + + test('adding a multi value query variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + await page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton).click(); + + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('a label'); + const descriptionInput = page.locator('[placeholder="Descriptive text"]'); + await descriptionInput.fill('a description'); + const datasourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.inputV2); + await datasourcePicker.fill('gdev-testdata'); + const datasourceList = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-testdata').click(); + const queryInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput + ); + await queryInput.fill('*'); + const regexInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2 + ); + await regexInput.fill('/.*C.*/'); + + // Enable Multi-value + const multiValueLabel = page.locator('label').filter({ hasText: 'Multi-value' }); + const multiValueCheckbox = multiValueLabel.locator('input[type="checkbox"]'); + await multiValueCheckbox.click({ force: true }); + await expect(multiValueCheckbox).toBeChecked(); + + // Enable Include All option + const includeAllLabel = page.locator('label').filter({ hasText: 'Include All option' }); + const includeAllCheckbox = includeAllLabel.locator('input[type="checkbox"]'); + await includeAllCheckbox.click({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + const customAllInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput + ); + await expect(customAllInput).toHaveValue(''); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption + ) + ).toHaveCount(2); + + // Navigate back to dashboard + const submitButton = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton + ); + await submitButton.click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // Check variable appears in submenu + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('a label')) + ).toBeVisible(); + + const submenuItems = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItems).toHaveCount(4); + + const fourthItem = submenuItems.nth(3); + const input = fourthItem.locator('input'); + await input.click(); + + // Check dropdown has 3 options (All + filtered results) + const options = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(options).toHaveCount(3); + + // Check toggle shows Selected (1) + const toggleAll = dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions); + await expect(toggleAll).toHaveText('Selected (1)'); + + // Check options contain 'All' and 'C' + await expect(options.filter({ hasText: 'All' })).toBeVisible(); + // need to use regex here else this will also match "Selected" + await expect(options.filter({ hasText: /^C$/ })).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts new file mode 100644 index 00000000000..4f5a856c5de --- /dev/null +++ b/e2e-playwright/dashboards-suite/new-text-box-variable.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; +const DASHBOARD_NAME = 'Test variable output'; + +test.describe( + 'Variables - Text box', + { + tag: ['@dashboards'], + }, + () => { + test('can add a new text box variable', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ orgId: '1', editview: 'variables' }), + }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + + // Create a new "text box" variable + await dashboardPage.getByGrafanaSelector(selectors.components.CallToActionCard.buttonV2('Add variable')).click(); + const typeSelect = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2 + ); + await typeSelect.locator('input').fill('Textbox'); + await typeSelect.locator('input').press('Enter'); + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2 + ); + await nameInput.fill('VariableUnderTest'); + const labelInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2 + ); + await labelInput.fill('Variable under test'); + const textBoxInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2 + ); + await textBoxInput.fill('cat-dog'); + + // Navigate back to the dashboard and change the selected variable value + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + + const textInput = submenuItem.locator('input'); + await textInput.fill('dog-cat'); + await textInput.blur(); + + // Assert it was rendered + await expect(page.locator('.markdown-html').first()).toContainText('VariableUnderTest: dog-cat'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts new file mode 100644 index 00000000000..22aacc41c88 --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-horizontally.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'WVpf2jp7z/repeating-a-panel-horizontally'; + +test.describe( + 'Repeating a panel horizontally', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat a panel horizontally', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + // Verify all panels are initially visible + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + } + + // Change to only show panels 1 + 3 + const horizontalVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('horizontal')) + .locator('..') + .locator('input'); + await horizontalVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown panels + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-horizontal=1&var-horizontal=3` }); + + let prevLeft = Number.NEGATIVE_INFINITY; + let prevTop: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + // Check correct panels are displayed with proper positioning + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(left).toBeGreaterThan(prevLeft); + if (prevTop !== null) { + expect(top).toBe(prevTop); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts new file mode 100644 index 00000000000..0e6755c05e5 --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-a-panel-vertically.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'OY8Ghjt7k/repeating-a-panel-vertically'; + +test.describe( + 'Repeating a panel vertically', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat a panel vertically', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const panelTitles = ['Panel Title 1', 'Panel Title 2', 'Panel Title 3']; + + // Verify all panels are initially visible + for (const title of panelTitles) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + } + + // Change to only show panels 1 + 3 + const verticalVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('vertical')) + .locator('..') + .locator('input'); + await verticalVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown panels + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-vertical=1&var-vertical=3` }); + + let prevTop = Number.NEGATIVE_INFINITY; + let prevLeft: number | null = null; + const panelsShown = ['Panel Title 1', 'Panel Title 3']; + const panelsNotShown = ['Panel Title 2']; + + // Check correct panels are displayed with proper positioning + for (const title of panelsShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeVisible(); + + const boundingBox = await panel.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { x: left, y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + if (prevLeft !== null) { + expect(left).toBe(prevLeft); + } + + prevLeft = left; + prevTop = top; + } + } + + for (const title of panelsNotShown) { + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title)); + await expect(panel).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts new file mode 100644 index 00000000000..c326f55552d --- /dev/null +++ b/e2e-playwright/dashboards-suite/repeating-an-empty-row.spec.ts @@ -0,0 +1,114 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'dtpl2Ctnk/repeating-an-empty-row'; + +test.describe( + 'Repeating empty rows', + { + tag: ['@dashboards'], + }, + () => { + test('should be able to repeat empty rows vertically', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + let prevTop = Number.NEGATIVE_INFINITY; + const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3']; + + for (const title of rowTitles) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + }); + + test('responds to changes to the variables', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + const rowTitles = ['Row title 1', 'Row title 2', 'Row title 3']; + + // Verify all rows are initially visible + for (const title of rowTitles) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + } + + // Change to only show rows 1 + 3 + const rowVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('row')) + .locator('..') + .locator('input'); + await rowVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3')) + .click(); + + // blur the dropdown + await page.locator('body').click(); + + // Verify positioning of shown rows + let prevTop = Number.NEGATIVE_INFINITY; + const rowsShown = ['Row title 1', 'Row title 3']; + const rowsNotShown = ['Row title 2']; + + for (const title of rowsShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + + for (const title of rowsNotShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeHidden(); + } + }); + + test('loads a dashboard based on the query params correctly', async ({ gotoDashboardPage, selectors }) => { + // Have to manually add the queryParams to the url because they have the same name + const dashboardPage = await gotoDashboardPage({ uid: `${PAGE_UNDER_TEST}?var-row=1&var-row=3` }); + + let prevTop = Number.NEGATIVE_INFINITY; + const rowsShown = ['Row title 1', 'Row title 3']; + const rowsNotShown = ['Row title 2']; + + // Check correct rows are displayed with proper positioning + for (const title of rowsShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeVisible(); + + const boundingBox = await row.boundingBox(); + expect(boundingBox).not.toBeNull(); + + if (boundingBox) { + const { y: top } = boundingBox; + expect(top).toBeGreaterThan(prevTop); + prevTop = top; + } + } + + for (const title of rowsNotShown) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(title)); + await expect(row).toBeHidden(); + } + }); + } +); diff --git a/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts new file mode 100644 index 00000000000..ccf6056d695 --- /dev/null +++ b/e2e-playwright/dashboards-suite/set-options-from-ui.spec.ts @@ -0,0 +1,280 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; + +test.describe( + 'Variables - Set options from ui', + { + tag: ['@dashboards'], + }, + () => { + test('clicking a value that is not part of dependents options should change these to All', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'A', + 'var-server': 'AA', + 'var-pod': 'AAA', + }), + }); + + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('..') + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + ) + ).toBeVisible(); + + const serverVariableAllOptions = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + ); + await expect(serverVariableAllOptions).toHaveCount(2); + + const firstServerDropdown = serverVariableAllOptions.nth(0); + await expect(firstServerDropdown).toBeVisible(); + await firstServerDropdown.locator('input').click(); + + const selectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(selectOptions).toHaveCount(10); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (1)' + ); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')) + ).toBeVisible(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('pod')) + .locator('..') + .locator('input'); + await podVariable.click(); + + // length is 11 because of virtualized select options + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option).locator('..'); + await expect(podSelectOptions).toHaveCount(11); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAC')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAD')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAE')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAF')) + ).toBeVisible(); + }); + + test('adding a value that is not part of dependents options should add the new values dependant options', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + orgId: '1', + 'var-datacenter': 'A', + 'var-server': 'AA', + 'var-pod': 'AAA', + }), + }); + + // Click on datacenter variable dropdown + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')) + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Select.toggleAllOptions)).toHaveText( + 'Selected (2)' + ); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Verify datacenter shows "A,B" + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B') + ) + ).toBeVisible(); + + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')) + .locator('input'); + await expect(serverVariable).toBeVisible(); + await serverVariable.click(); + + const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(serverSelectOptions).toHaveCount(11); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD')) + ).toBeVisible(); + + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Click on pod variable dropdown + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA')) + .locator('input'); + await expect(podVariable).toBeVisible(); + await podVariable.click(); + + // Verify pod dropdown has 10 options + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(podSelectOptions).toHaveCount(10); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAC')) + ).toBeVisible(); + }); + + test('removing a value that is part of dependents options should remove the new values dependant options', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ + // can't use queryParams here because it won't work with the multiple values for each variable + uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-datacenter=B&var-server=AA&var-server=BB&var-pod=AAA&var-pod=BBB`, + }); + + // Click on datacenter variable dropdown + const datacenterVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B')) + .locator('input'); + await expect(datacenterVariable).toBeVisible(); + await datacenterVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')) + .click(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + ) + ).toBeVisible(); + + // Click on server variable dropdown + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')) + .locator('input'); + await expect(serverVariable).toBeVisible(); + await serverVariable.click(); + + const serverSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(serverSelectOptions).toHaveCount(10); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')) + ).toBeVisible(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Click on pod variable dropdown + const podVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')) + .locator('input'); + await expect(podVariable).toBeVisible(); + await podVariable.click(); + + const podSelectOptions = dashboardPage.getByGrafanaSelector(selectors.components.Select.option); + await expect(podSelectOptions).toHaveCount(10); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC')) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/snapshot-create.spec.ts b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts new file mode 100644 index 00000000000..a2ae416d84b --- /dev/null +++ b/e2e-playwright/dashboards-suite/snapshot-create.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'ZqZnVvFZz'; + +test.use({ + featureToggles: { + newDashboardSharingComponent: false, // Use legacy sharing component for this test + }, +}); + +test.describe( + 'Snapshots', + { + tag: ['@dashboards'], + }, + () => { + test('Create a snapshot dashboard', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_UID, + }); + + const panelsToCheck = [ + 'Raw Data Graph', + 'Last non-null', + 'min', + 'Max', + 'The data from graph above with seriesToColumns transform', + ]; + + // Open the sharing modal + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.shareDashboard).click(); + + // Select the snapshot tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Snapshot')).click(); + + // Publish snapshot + await dashboardPage + .getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.PublishSnapshot) + .click(); + + // Copy link button should be visible + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlButton) + ).toBeVisible(); + + // Get the snapshot URL from the input field + const urlInput = dashboardPage.getByGrafanaSelector( + selectors.pages.ShareDashboardModal.SnapshotScene.CopyUrlInput + ); + const snapshotUrl = await urlInput.inputValue(); + + // Extract the snapshot key from the URL and navigate to the snapshot + const snapshotKey = getSnapshotKey(snapshotUrl); + await page.goto(`/dashboard/snapshot/${snapshotKey}`); + + // Validate the dashboard controls are rendered + await expect(dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls)).toBeVisible(); + + // Validate the panels are rendered + for (const title of panelsToCheck) { + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(title))).toBeVisible(); + } + }); + } +); + +const getSnapshotKey = (url: string): string => { + return url.split('/').pop() || ''; +}; diff --git a/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts new file mode 100644 index 00000000000..54bfdb771ba --- /dev/null +++ b/e2e-playwright/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_UID = 'yBCC3aKGk'; + +test.describe( + 'Templating dashboard links and variables', + { + tag: ['@dashboards'], + }, + () => { + test('Tests dashboard links and variables in links', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID }); + + const verifyLinks = async (variableValue: string) => { + const dashboardLinks = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.link); + + let linkCount = 0; + await expect + .poll(async () => { + linkCount = await dashboardLinks.count(); + return linkCount; + }) + .toBeGreaterThan(13); + + for (let i = 0; i < linkCount; i++) { + const href = await dashboardLinks.nth(i).getAttribute('href'); + expect(href).toContain(variableValue); + } + }; + + // Click on dashboard links dropdown + const dashboardLinksDropdown = dashboardPage.getByGrafanaSelector(selectors.components.DashboardLinks.dropDown); + await expect(dashboardLinksDropdown).toBeVisible(); + await dashboardLinksDropdown.click(); + + // Verify links contain default variable value + await verifyLinks('var-custom=$__all'); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Change variable value from $__all to p2 + const customVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all')) + .locator('input'); + await expect(customVariable).toBeVisible(); + await customVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2')) + .click(); + + // Close dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // Open dashboard links dropdown again + await expect(dashboardLinksDropdown).toBeVisible(); + await dashboardLinksDropdown.click(); + + // Verify all links now contain the p2 value + await verifyLinks('p2'); + }); + } +); diff --git a/e2e-playwright/dashboards-suite/textbox-variables.spec.ts b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts new file mode 100644 index 00000000000..20815446223 --- /dev/null +++ b/e2e-playwright/dashboards-suite/textbox-variables.spec.ts @@ -0,0 +1,56 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'AejrN1AMz'; + +test.describe( + 'TextBox - load options scenarios', + { + tag: ['@dashboards'], + }, + () => { + test('default options should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + await validateTextboxAndMarkup(page, dashboardPage, selectors, 'default value'); + }); + + test('loading variable from url should be correct', async ({ page, gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: PAGE_UNDER_TEST, + queryParams: new URLSearchParams({ + 'var-text': 'not default value', + }), + }); + + await validateTextboxAndMarkup(page, dashboardPage, selectors, 'not default value'); + }); + } +); + +// Helper function to validate textbox and markup +async function validateTextboxAndMarkup( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + value: string +) { + const submenuItem = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItem); + await expect(submenuItem).toBeVisible(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('text')) + ).toBeVisible(); + + const textInput = submenuItem.locator('input'); + await expect(textInput).toBeVisible(); + await expect(textInput).toHaveValue(value); + + const textPanel = page.locator(selectors.components.Panels.Visualization.Text.container('')); + await expect(textPanel).toBeVisible(); + + const headerElement = textPanel.locator('h1'); + await expect(headerElement).toBeVisible(); + await expect(headerElement).toHaveText(`variable: ${value}`); +} diff --git a/e2e-playwright/dashboards-suite/utils/makeDashboard.ts b/e2e-playwright/dashboards-suite/utils/makeDashboard.ts new file mode 100644 index 00000000000..2ada48db7a9 --- /dev/null +++ b/e2e-playwright/dashboards-suite/utils/makeDashboard.ts @@ -0,0 +1,58 @@ +export function makeNewDashboardRequestBody(dashboardName: string, folderUid?: string) { + return { + dashboard: { + annotations: { + list: [ + { + builtIn: 1, + datasource: { type: 'grafana', uid: '-- Grafana --' }, + enable: true, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + type: 'dashboard', + }, + ], + }, + editable: true, + fiscalYearStartMonth: 0, + graphTooltip: 0, + links: [], + liveNow: false, + panels: [ + { + datasource: { type: 'testdata', uid: '89_jzlT4k' }, + gridPos: { h: 9, w: 12, x: 0, y: 0 }, + id: 2, + options: { + code: { + language: 'plaintext', + showLineNumbers: false, + showMiniMap: false, + }, + content: '***A nice little happy empty dashboard***', + mode: 'markdown', + }, + pluginVersion: '9.4.0-pre', + title: 'Nothing to see here', + type: 'text', + }, + ], + refresh: '', + revision: 1, + schemaVersion: 38, + tags: [], + templating: { list: [] }, + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + timezone: '', + title: dashboardName, + version: 0, + uid: '', + weekStart: '', + }, + message: '', + overwrite: false, + folderUid, + } as const; +} diff --git a/e2e-playwright/dashboards/DashboardLiveTest.json b/e2e-playwright/dashboards/DashboardLiveTest.json new file mode 100644 index 00000000000..7d98377df89 --- /dev/null +++ b/e2e-playwright/dashboards/DashboardLiveTest.json @@ -0,0 +1,107 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.3.0-pre", + "targets": [ + { + "channel": "plugin/testdata/random-20Hz-stream", + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "measurements", + "refId": "A" + } + ], + "title": "Live", + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "New dashboard", + "version": 0, + "uid": "live-e2e-test", + "weekStart": "" +} diff --git a/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json b/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json new file mode 100644 index 00000000000..08b2e3fda11 --- /dev/null +++ b/e2e-playwright/dashboards/DataLinkWithoutSlugTest.json @@ -0,0 +1,255 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "links": [ + { + "title": "", + "url": "/d/${__dashboard.uid}?var-instance=${__data.fields.test1}&${__url_time_range}" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "alias": "test1", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv" + } + ], + "title": "Data links without slug", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "counters_logins{geohash=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + "name": "instance", + "options": [ + { + "selected": true, + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + { + "selected": false, + "text": "9yy21uzzxypg", + "value": "9yy21uzzxypg" + }, + { + "selected": false, + "text": "dr199bpvpcru", + "value": "dr199bpvpcru" + }, + { + "selected": false, + "text": "dre33fzyxcrz", + "value": "dre33fzyxcrz" + }, + { + "selected": false, + "text": "gc6j7crvrcpf", + "value": "gc6j7crvrcpf" + }, + { + "selected": false, + "text": "u6g9zuxvxypv", + "value": "u6g9zuxvxypv" + } + ], + "query": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv", + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "Data Link without slug test", + "uid": "data-link-no-slug", + "version": 3 +} diff --git a/e2e-playwright/dashboards/PanelSandboxDashboard.json b/e2e-playwright/dashboards/PanelSandboxDashboard.json new file mode 100644 index 00000000000..50fb2ad7f57 --- /dev/null +++ b/e2e-playwright/dashboards/PanelSandboxDashboard.json @@ -0,0 +1,57 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "title": "Sandbox Panel test", + "type": "sandbox-test-panel" + } + ], + "refresh": "", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Sandbox Panel Test", + "uid": "c46b2460-16b7-42a5-82d1-b07fbf431950", + "version": 1, + "weekStart": "" +} diff --git a/e2e-playwright/dashboards/TestDashboard.json b/e2e-playwright/dashboards/TestDashboard.json new file mode 100644 index 00000000000..8f58d09c697 --- /dev/null +++ b/e2e-playwright/dashboards/TestDashboard.json @@ -0,0 +1,208 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- 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, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "text": {} + }, + "pluginVersion": "8.3.0-pre", + "title": "Gauge Example", + "type": "gauge" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "8.3.0-pre", + "title": "Stat", + "type": "stat" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "title": "Time series example", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 31, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2021-09-01T04:00:00.000Z", + "to": "2021-09-15T04:00:00.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "E2E Test - Import Dashboard", + "uid": "kquZN5H7k", + "version": 4 +} diff --git a/e2e-playwright/dashboards/TestV2Dashboard.json b/e2e-playwright/dashboards/TestV2Dashboard.json new file mode 100644 index 00000000000..da89762c3a6 --- /dev/null +++ b/e2e-playwright/dashboards/TestV2Dashboard.json @@ -0,0 +1,424 @@ +{ + "apiVersion": "dashboard.grafana.app/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "admjzp8", + "namespace": "default", + "uid": "hrbekBWXeM7nC7GtouIbcngFyuqt8Xx7KlE4AnTwV7AX", + "resourceVersion": "1", + "generation": 1, + "creationTimestamp": "2025-05-16T09:41:56Z", + "labels": { + "grafana.app/deprecatedInternalID": "182" + }, + "annotations": { + "grafana.app/createdBy": "user:cejvsh18uudxcf", + "grafana.app/updatedBy": "user:cejvsh18uudxcf", + "grafana.app/updatedTimestamp": "2025-05-16T09:41:56Z", + "grafana.app/folder": "" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts" + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "hidden": false, + "query": { + "kind": "grafana-testdata-datasource", + "spec": {} + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "New panel", + "vizConfig": { + "kind": "timeseries", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.0-pre" + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 8 + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "New Test V2 Dashboard", + "variables": [] + }, + "status": {} +} diff --git a/e2e-playwright/fixtures/exemplars-query-response.json b/e2e-playwright/fixtures/exemplars-query-response.json new file mode 100644 index 00000000000..6c26a9fbf7d --- /dev/null +++ b/e2e-playwright/fixtures/exemplars-query-response.json @@ -0,0 +1,323 @@ +{ + "results": { + "A": { + "frames": [ + { + "schema": { + "name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))", + "refId": "A", + "meta": { "custom": { "resultType": "matrix" } }, + "fields": [ + { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, + { + "name": "Value", + "type": "number", + "typeInfo": { "frame": "float64" }, + "labels": {}, + "config": { + "displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))" + } + } + ] + }, + "data": { + "values": [ + [ + 1633619595000, 1633619610000, 1633619625000, 1633619640000, 1633619655000, 1633619670000, 1633619685000, + 1633619700000, 1633619715000, 1633619730000, 1633619745000, 1633619760000, 1633619775000, 1633619790000, + 1633619805000, 1633619820000, 1633619835000, 1633619850000, 1633619865000, 1633619880000, 1633619895000 + ], + [ + 0.07245212135073513, 0.07253198890830721, 0.07247862573797707, 0.07238248338231042, 0.07221687487740913, + 0.07223291298743946, 0.07225427016727755, 0.024531677091864545, 0.02317081920915543, + 0.07548902139580993, 0.0777721702857508, 0.07768649905047344, 0.07782257603228229, 0.07788810213200052, + 0.07791835055437593, 0.07798387201529966, 0.07790826751849372, 0.07794858648610933, 0.07778729925797964, + 0.07769657495236215, 0.077550401329267 + ] + ] + } + }, + { + "schema": { + "name": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))", + "refId": "A", + "meta": { "custom": { "resultType": "vector" } }, + "fields": [ + { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, + { + "name": "Value", + "type": "number", + "typeInfo": { "frame": "float64" }, + "labels": {}, + "config": { + "displayNameFromDS": "histogram_quantile(0.95, sum(rate(tns_request_duration_seconds_bucket[5m])) by (le))" + } + } + ] + }, + "data": { "values": [[1633619900000], [0.0775504013292671]] } + }, + { + "schema": { + "name": "exemplar", + "refId": "A", + "meta": { "custom": { "resultType": "exemplar" } }, + "fields": [ + { "name": "Time", "type": "time", "typeInfo": { "frame": "time.Time" } }, + { "name": "Value", "type": "number", "typeInfo": { "frame": "float64" } }, + { "name": "instance", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "__name__", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "job", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "status_code", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "method", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "traceID", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "route", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "ws", "type": "string", "typeInfo": { "frame": "string" } }, + { "name": "le", "type": "string", "typeInfo": { "frame": "string" } } + ] + }, + "data": { + "values": [ + [ + 1633619598000, 1633619622000, 1633619625000, 1633619646000, 1633619658000, 1633619682000, 1633619695000, + 1633619712000, 1633619712000, 1633619724000, 1633619717000, 1633619742000, 1633619757000, 1633619771000, + 1633619784000, 1633619801000, 1633619806000, 1633619833000, 1633619833000, 1633619845000, 1633619862000, + 1633619877000, 1633619889000 + ], + [ + 0.0146153, 0.0118506, 0.0473847, 0.026997, 0.0164318, 0.0113532, 0.0105197, 0.162789, 0.0556026, + 0.148856, 0.0433809, 0.0117758, 0.0114496, 0.0114099, 0.0421927, 0.0134148, 0.0152827, 0.6975967, + 0.0394788, 0.0137441, 0.0110939, 0.0104496, 0.0101284 + ], + [ + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "db:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80", + "app:80" + ], + [ + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket", + "tns_request_duration_seconds_bucket" + ], + [ + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/db", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app", + "tns/app" + ], + [ + "302", + "200", + "200", + "200", + "200", + "200", + "200", + "500", + "200", + "302", + "208", + "200", + "200", + "200", + "200", + "200", + "302", + "200", + "200", + "200", + "200", + "200", + "200" + ], + [ + "POST", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET", + "POST", + "POST", + "GET", + "GET", + "GET", + "GET", + "GET", + "POST", + "GET", + "GET", + "GET", + "GET", + "GET", + "GET" + ], + [ + "6a3cf561ef6c32a0", + "396bcdf29601a149", + "57c04ef608f11158", + "77c757dab83c665f", + "3d1069567e873f5e", + "b337949f6213efd", + "21b20cbe533cf099", + "2c10b3aa30fabd66", + "42ac6088a757636b", + "2f81158008cd4dcc", + "320b803ad7323b37", + "7f15fd82aeb8b361", + "11c79266da8a74cd", + "5a8571bdcc04c990", + "3de3f4f42ccb93ae", + "23343ac91cc0638", + "5cea3aad17ab11c8", + "5d334e2843d3405a", + "3cf6834596d4b6b6", + "1ab6cff012959723", + "2f78bc2c398b8b20", + "6d5862a70c3abd42", + "f5421be4054f501" + ], + [ + "post", + "root", + "root", + "root", + "root", + "root", + "root", + "root", + "root", + "post", + "post", + "root", + "root", + "root", + "root", + "root", + "post", + "metrics", + "root", + "root", + "root", + "root", + "root" + ], + [ + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false", + "false" + ], + [ + "0.025", + "0.025", + "0.05", + "0.05", + "0.025", + "0.025", + "0.025", + "0.25", + "0.1", + "0.25", + "0.05", + "0.025", + "0.025", + "0.025", + "0.05", + "0.025", + "0.025", + "1.0", + "0.05", + "0.025", + "0.025", + "0.025", + "0.025" + ] + ] + } + } + ] + } + } +} diff --git a/e2e-playwright/fixtures/long-trace-response.json b/e2e-playwright/fixtures/long-trace-response.json new file mode 100644 index 00000000000..80955535638 --- /dev/null +++ b/e2e-playwright/fixtures/long-trace-response.json @@ -0,0 +1,7592 @@ +{ + "data": [ + { + "traceID": "3fa414edcef6ad90", + "spans": [ + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b26effbab24e95a", + "operationName": "FindTraceByID", + "references": [], + "startTime": 1605873894680581, + "duration": 1820, + "tags": [ + { "key": "component", "type": "string", "value": "gRPC" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0f5c1808567e4403", + "operationName": "FindTraceByID", + "references": [], + "startTime": 1605873894680587, + "duration": 1847, + "tags": [ + { "key": "component", "type": "string", "value": "gRPC" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "59f093577238d61e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683862, + "duration": 10204, + "tags": [], + "logs": [ + { "timestamp": 1605873894683872, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894694063, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1cc731490b1da4c5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683858, + "duration": 10257, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "602204dc8b8fbc6d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683201, + "duration": 11185, + "tags": [], + "logs": [ + { "timestamp": 1605873894683207, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894694385, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "586e5e4c0400de11", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683196, + "duration": 11200, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "779ac3811ce65e40", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683844, + "duration": 10983, + "tags": [ + { "key": "blockID", "type": "string", "value": "20a16df1-a312-4b1a-a2e2-33b55e9f3c8b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894694822, + "fields": [ + { "key": "bytes", "type": "int64", "value": 315664 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "24203526fe09b1e2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682997, + "duration": 12453, + "tags": [], + "logs": [ + { "timestamp": 1605873894683002, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894695448, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0afe9ad5f5b01be7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682993, + "duration": 12466, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "51413d67348a4624", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682986, + "duration": 13059, + "tags": [ + { "key": "blockID", "type": "string", "value": "08b90b09-c56e-4b4a-b95f-3f0409dc9ce9" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894695963, + "fields": [ + { "key": "bytes", "type": "int64", "value": 239824 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "60007a76ffde4644", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682866, + "duration": 13279, + "tags": [], + "logs": [ + { "timestamp": 1605873894682872, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894696144, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "09d7a8c1faef5a84", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682861, + "duration": 13291, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2755efbbfb1b537b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682846, + "duration": 14054, + "tags": [ + { "key": "blockID", "type": "string", "value": "f78b0397-d3ad-4514-9bf4-87b6ea7e920e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894696898, + "fields": [ + { "key": "bytes", "type": "int64", "value": 218440 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25223420e121413a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683188, + "duration": 14278, + "tags": [ + { "key": "blockID", "type": "string", "value": "3ae22086-9266-481a-9725-c921471e4a94" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894697462, + "fields": [ + { "key": "bytes", "type": "int64", "value": 397880 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "17a3baf85848a727", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683030, + "duration": 14724, + "tags": [], + "logs": [ + { "timestamp": 1605873894683033, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894697752, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46ebfa6c443776c4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683027, + "duration": 14734, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "19b1afe02cf639cf", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683883, + "duration": 14279, + "tags": [], + "logs": [ + { "timestamp": 1605873894683889, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698160, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e5a7dd55283f907", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683879, + "duration": 14289, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5085badf0c1dc842", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683657, + "duration": 14886, + "tags": [], + "logs": [ + { "timestamp": 1605873894683663, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698542, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "71a0e94722b662ed", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683653, + "duration": 14897, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57e69d8f17b39563", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683388, + "duration": 15548, + "tags": [], + "logs": [ + { "timestamp": 1605873894683394, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698936, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6fe636103f47e1fc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683384, + "duration": 15558, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "52146a5c1b2c0030", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683284, + "duration": 15701, + "tags": [], + "logs": [ + { "timestamp": 1605873894683290, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894698984, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "160fb4c8329a2ea0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683280, + "duration": 15712, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1e283fe0dd8cc773", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683024, + "duration": 16029, + "tags": [ + { "key": "blockID", "type": "string", "value": "9e102b4e-115a-4bda-abd6-aa6221f9e4b7" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699050, + "fields": [ + { "key": "bytes", "type": "int64", "value": 395808 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bae5c35dd7187ba", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683644, + "duration": 15612, + "tags": [ + { "key": "blockID", "type": "string", "value": "b2f5a951-19a0-473d-8830-e1120ab7bf25" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699255, + "fields": [ + { "key": "bytes", "type": "int64", "value": 345992 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5af2c497b60703d9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683842, + "duration": 15628, + "tags": [], + "logs": [ + { "timestamp": 1605873894683848, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894699469, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a64d382dd0239a7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683837, + "duration": 15639, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "04652166eaec115c", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683378, + "duration": 16179, + "tags": [ + { "key": "blockID", "type": "string", "value": "30903640-5e8c-4cf6-9dc8-f84e0e2541c8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699555, + "fields": [ + { "key": "bytes", "type": "int64", "value": 291056 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "650c7f5ec8cc53a5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683871, + "duration": 15807, + "tags": [ + { "key": "blockID", "type": "string", "value": "19b49abb-e17a-4632-a4b9-3ce95208e3cf" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894699675, + "fields": [ + { "key": "bytes", "type": "int64", "value": 424248 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b30323ce39314b9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684553, + "duration": 15144, + "tags": [], + "logs": [ + { "timestamp": 1605873894684559, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894699696, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "288816ad36c9020c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684549, + "duration": 15154, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "26e83a54365218ad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683881, + "duration": 16602, + "tags": [], + "logs": [ + { "timestamp": 1605873894683888, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894700482, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e6a2e62081720fd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683878, + "duration": 16613, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "63332243ceed106c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683893, + "duration": 16666, + "tags": [], + "logs": [ + { "timestamp": 1605873894683900, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894700557, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7dbbbda52a6d32ce", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683888, + "duration": 16678, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "195ed27075e44238", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683828, + "duration": 16766, + "tags": [ + { "key": "blockID", "type": "string", "value": "6c5d1290-2b4b-4f33-9798-63b6654e16b4" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894700591, + "fields": [ + { "key": "bytes", "type": "int64", "value": 367848 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "35e5a12a53c6088a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682748, + "duration": 17901, + "tags": [], + "logs": [ + { "timestamp": 1605873894682751, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894700647, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "690fcd8c8dc87ae8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683273, + "duration": 17376, + "tags": [ + { "key": "blockID", "type": "string", "value": "f1db0c64-befe-4790-af19-7b48e57a9558" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894700646, + "fields": [ + { "key": "bytes", "type": "int64", "value": 386448 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "113befce4abfecb2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682745, + "duration": 17911, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "277870fa55872b13", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683865, + "duration": 17440, + "tags": [ + { "key": "blockID", "type": "string", "value": "f05f1d13-0250-492a-abc8-bca24ccf3a15" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894701291, + "fields": [ + { "key": "bytes", "type": "int64", "value": 211672 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "022b6c95374f166d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683879, + "duration": 17471, + "tags": [ + { "key": "blockID", "type": "string", "value": "0cba7eaf-2546-41ac-99d7-673ef23d6e98" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894701347, + "fields": [ + { "key": "bytes", "type": "int64", "value": 406456 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6cee3530fc730d34", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684631, + "duration": 16733, + "tags": [], + "logs": [ + { "timestamp": 1605873894684639, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894701363, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "674b435291a256c4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684627, + "duration": 16745, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1de85b574e5d906c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683373, + "duration": 18328, + "tags": [], + "logs": [ + { "timestamp": 1605873894683380, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894701701, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4c5ac8757f9888b7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683369, + "duration": 18338, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3e5ab83b57207c74", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683042, + "duration": 18823, + "tags": [], + "logs": [ + { "timestamp": 1605873894683045, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894701863, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7d9927e5c258d511", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682730, + "duration": 19136, + "tags": [ + { "key": "blockID", "type": "string", "value": "794e2adc-701e-4c2d-907a-66221b4455d3" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894701864, + "fields": [ + { "key": "bytes", "type": "int64", "value": 289928 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6c9178ed1e68f858", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683039, + "duration": 18834, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "445d4f3f2dc4d0ad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684214, + "duration": 17919, + "tags": [], + "logs": [ + { "timestamp": 1605873894684221, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702132, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "30dd998b2082f2b9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684210, + "duration": 17958, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2ff9bbb6c991a0ea", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683877, + "duration": 18301, + "tags": [], + "logs": [ + { "timestamp": 1605873894683883, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702177, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "310a2399bb07e8bd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683873, + "duration": 18311, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "19021bbbe6310785", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683360, + "duration": 18978, + "tags": [ + { "key": "blockID", "type": "string", "value": "d2212e62-5b1a-41e2-ae43-c0a596125f1b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894702335, + "fields": [ + { "key": "bytes", "type": "int64", "value": 199208 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "021f72c9979124b5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684621, + "duration": 17816, + "tags": [ + { "key": "blockID", "type": "string", "value": "06ebaf3b-4501-4cda-91fb-c48a9d33a99c" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894702434, + "fields": [ + { "key": "bytes", "type": "int64", "value": 384696 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "68a1e78424019eb9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683657, + "duration": 18900, + "tags": [], + "logs": [ + { "timestamp": 1605873894683663, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702556, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "244e73561d0c691d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683653, + "duration": 18910, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5c1d1b2d38dddcfb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683518, + "duration": 19222, + "tags": [], + "logs": [ + { "timestamp": 1605873894683526, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702739, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "364583eecf36b543", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683513, + "duration": 19232, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "22e42286de359dc4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683843, + "duration": 18969, + "tags": [], + "logs": [ + { "timestamp": 1605873894683849, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702812, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7b936283fac4d0ac", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683838, + "duration": 18981, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "660886869edd36cf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684202, + "duration": 18627, + "tags": [ + { "key": "blockID", "type": "string", "value": "f07137b8-7a0b-4199-b1a7-6b7d5b230723" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894702824, + "fields": [ + { "key": "bytes", "type": "int64", "value": 293936 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57ed8902af3a60b5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683549, + "duration": 19426, + "tags": [], + "logs": [ + { "timestamp": 1605873894683554, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894702972, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "64cadcdb4f18b2f7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683544, + "duration": 19437, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25f434fb5960aaef", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683933, + "duration": 19303, + "tags": [], + "logs": [ + { "timestamp": 1605873894683939, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894703235, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62afac560d435620", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683929, + "duration": 19314, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "58435ec74d79cc93", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683865, + "duration": 19469, + "tags": [ + { "key": "blockID", "type": "string", "value": "f2a53e6e-e261-4ec2-92bd-97c5a4c4b760" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894703331, + "fields": [ + { "key": "bytes", "type": "int64", "value": 390648 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "578849d0d44400b5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684004, + "duration": 19335, + "tags": [], + "logs": [ + { "timestamp": 1605873894684012, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894703337, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1f5faebfb90378ad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683999, + "duration": 19346, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "41f1eb48b61ef185", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683035, + "duration": 20463, + "tags": [ + { "key": "blockID", "type": "string", "value": "941a63d4-2739-4ba2-9a15-08256b5c9eae" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894703490, + "fields": [ + { "key": "bytes", "type": "int64", "value": 438128 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "71ee8c7b83046da0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683645, + "duration": 19895, + "tags": [ + { "key": "blockID", "type": "string", "value": "151c489c-a86a-49b7-9fa9-31d1714d59ee" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894703538, + "fields": [ + { "key": "bytes", "type": "int64", "value": 325344 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bf030a07aaceb80", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683218, + "duration": 20692, + "tags": [], + "logs": [ + { "timestamp": 1605873894683225, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894703909, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "54b34afd73af12d1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683215, + "duration": 21011, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a7ba0261825c53c", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683505, + "duration": 20615, + "tags": [ + { "key": "blockID", "type": "string", "value": "db0fa030-4607-40e5-998b-47029aa3430e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704118, + "fields": [ + { "key": "bytes", "type": "int64", "value": 411272 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a597269b23b1bcb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683426, + "duration": 20720, + "tags": [], + "logs": [ + { "timestamp": 1605873894683431, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894704146, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66d886579510b6fd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683422, + "duration": 20841, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1ef8e63340342174", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683990, + "duration": 20334, + "tags": [ + { "key": "blockID", "type": "string", "value": "a10ec85d-9fd2-403e-abcd-6f4ec49b0396" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704322, + "fields": [ + { "key": "bytes", "type": "int64", "value": 442360 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46c6de90778460b1", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683830, + "duration": 20675, + "tags": [ + { "key": "blockID", "type": "string", "value": "7e9e0142-15ff-461e-8e05-6c62d920603a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704502, + "fields": [ + { "key": "bytes", "type": "int64", "value": 465744 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "686f3e58fe28940f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696113, + "duration": 8480, + "tags": [], + "logs": [ + { "timestamp": 1605873894696126, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894704591, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5960c1f5750b1cde", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696104, + "duration": 8495, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6aa5ddd42d96f825", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683921, + "duration": 20886, + "tags": [ + { "key": "blockID", "type": "string", "value": "43e5ad4f-11d6-4f25-9925-652cb801fd58" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704804, + "fields": [ + { "key": "bytes", "type": "int64", "value": 405344 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "166377800e8e82a7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683214, + "duration": 21686, + "tags": [], + "logs": [ + { "timestamp": 1605873894683221, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894704899, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e84f8676ef1efad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683209, + "duration": 21696, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "713c834576a0d9b0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683037, + "duration": 22197, + "tags": [], + "logs": [ + { "timestamp": 1605873894683045, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894705234, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "209c0e336c71e932", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683033, + "duration": 22209, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bd34d50efadb568", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683414, + "duration": 21894, + "tags": [ + { "key": "blockID", "type": "string", "value": "b432160f-347c-41ad-882e-f1786e4b42b1" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705305, + "fields": [ + { "key": "bytes", "type": "int64", "value": 409104 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "58cee6c544e69e4f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683206, + "duration": 22173, + "tags": [ + { "key": "blockID", "type": "string", "value": "b12afd19-298a-443f-97ce-b5b2e5bc9d79" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705375, + "fields": [ + { "key": "bytes", "type": "int64", "value": 376872 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4e48e93f70e06522", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696061, + "duration": 9466, + "tags": [ + { "key": "blockID", "type": "string", "value": "45701f45-c93a-4c35-9fed-9cce2c316a19" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705524, + "fields": [ + { "key": "bytes", "type": "int64", "value": 453296 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2422bf6c2ed108c2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683536, + "duration": 20571, + "tags": [ + { "key": "blockID", "type": "string", "value": "51945006-c165-40af-baea-769b3199bf46" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894704104, + "fields": [ + { "key": "bytes", "type": "int64", "value": 342200 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42fac7c66e0ca970", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683200, + "duration": 22685, + "tags": [ + { "key": "blockID", "type": "string", "value": "8772aa40-3489-4b12-b685-9f708ae4de75" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894705882, + "fields": [ + { "key": "bytes", "type": "int64", "value": 407152 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a86d93e70a1720c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684627, + "duration": 21313, + "tags": [], + "logs": [ + { "timestamp": 1605873894684633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894705939, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "72991150a8c3cf08", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684622, + "duration": 21322, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3ceac51ce73f994e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683627, + "duration": 22375, + "tags": [], + "logs": [ + { "timestamp": 1605873894683633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706001, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "704707012227a4f1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683623, + "duration": 22386, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "26cf501f6dcbb968", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683959, + "duration": 22090, + "tags": [], + "logs": [ + { "timestamp": 1605873894683965, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706048, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7ebb1c9d8a55ac56", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683952, + "duration": 22104, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bd01ea1e13ac6fd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683733, + "duration": 22571, + "tags": [], + "logs": [ + { "timestamp": 1605873894683739, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706303, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4f94f7e28081e1af", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683728, + "duration": 22582, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "60fd2b3931676856", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684310, + "duration": 22119, + "tags": [], + "logs": [ + { "timestamp": 1605873894684317, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706428, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "432bc11447588912", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684305, + "duration": 22131, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1e1aa88072a7cefc", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683026, + "duration": 23483, + "tags": [ + { "key": "blockID", "type": "string", "value": "9064347a-7c49-48d8-b348-8d734f7fd542" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894706506, + "fields": [ + { "key": "bytes", "type": "int64", "value": 365672 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1fb49823a6f803bf", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682930, + "duration": 23695, + "tags": [], + "logs": [ + { "timestamp": 1605873894682935, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706622, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7db786f0da6d756d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682926, + "duration": 23705, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01cb21bacc3933da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894697497, + "duration": 9150, + "tags": [], + "logs": [ + { "timestamp": 1605873894697507, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706646, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "260399c49430577a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894697488, + "duration": 9166, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5ba9d86263fc6da1", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684614, + "duration": 22250, + "tags": [ + { "key": "blockID", "type": "string", "value": "ca346cf4-8162-49e5-a0d0-0619d3813794" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894706862, + "fields": [ + { "key": "bytes", "type": "int64", "value": 416472 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "77f27a840cd8b75b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685061, + "duration": 21838, + "tags": [], + "logs": [ + { "timestamp": 1605873894685068, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706897, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4a48a86f95e117f9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685057, + "duration": 21850, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7d1f782957acfe32", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682777, + "duration": 24168, + "tags": [], + "logs": [ + { "timestamp": 1605873894682783, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894706944, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "77f8165c15176536", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682773, + "duration": 24206, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7f20dbc684de78c8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683615, + "duration": 23703, + "tags": [ + { "key": "blockID", "type": "string", "value": "f518974f-2e1e-41c8-b70c-cd2088f5a081" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707316, + "fields": [ + { "key": "bytes", "type": "int64", "value": 278728 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70a453eeff8ec687", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684828, + "duration": 22510, + "tags": [], + "logs": [ + { "timestamp": 1605873894684835, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894707337, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0c7d975a67c6d7bc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684823, + "duration": 22520, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7ccb153793c6afd9", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683720, + "duration": 23911, + "tags": [ + { "key": "blockID", "type": "string", "value": "6f72b73b-c5fe-4761-b91f-b92f447441fa" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707629, + "fields": [ + { "key": "bytes", "type": "int64", "value": 451984 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5bf10b9afef405a9", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894697476, + "duration": 10175, + "tags": [ + { "key": "blockID", "type": "string", "value": "61e0a11e-5e88-49c4-ad1d-81636670e642" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707648, + "fields": [ + { "key": "bytes", "type": "int64", "value": 296328 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1aae38562e2b6a1f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683945, + "duration": 23883, + "tags": [ + { "key": "blockID", "type": "string", "value": "7dda9580-666b-42f9-b8a1-1680a0de352f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707823, + "fields": [ + { "key": "bytes", "type": "int64", "value": 402936 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1dc5a0697b5d6161", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682764, + "duration": 25091, + "tags": [ + { "key": "blockID", "type": "string", "value": "bf101e70-4a86-4d88-890c-e976330ba857" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894707853, + "fields": [ + { "key": "bytes", "type": "int64", "value": 385288 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3df0c4e2de834172", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684425, + "duration": 23557, + "tags": [], + "logs": [ + { "timestamp": 1605873894684432, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894707980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "50f5d53109a047da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684421, + "duration": 23568, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "64e62db2206bdda3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682595, + "duration": 25553, + "tags": [], + "logs": [ + { "timestamp": 1605873894682603, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894708147, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "40f0742ab8be92ab", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682589, + "duration": 25564, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b89efb6b9fb16fc", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684815, + "duration": 23341, + "tags": [ + { "key": "blockID", "type": "string", "value": "84b0a7ea-895d-49f9-892c-11f689f0c13f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894708154, + "fields": [ + { "key": "bytes", "type": "int64", "value": 424816 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "33c05fda4c7d3921", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684414, + "duration": 23815, + "tags": [], + "logs": [ + { "timestamp": 1605873894684420, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894708228, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "742995638b3636e6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684409, + "duration": 23825, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1cf9294062a5780b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682920, + "duration": 25427, + "tags": [ + { "key": "blockID", "type": "string", "value": "e43ee3db-63c9-4d2b-a791-99a5a9203e4e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894708341, + "fields": [ + { "key": "bytes", "type": "int64", "value": 396312 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6852631d2c6d1586", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683843, + "duration": 24695, + "tags": [], + "logs": [ + { "timestamp": 1605873894683850, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894708538, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1691ee4e1f907b39", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683839, + "duration": 24706, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bcd55e85df0601a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684400, + "duration": 24493, + "tags": [ + { "key": "blockID", "type": "string", "value": "211313f2-7284-43eb-b9dc-134b5b344524" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894708891, + "fields": [ + { "key": "bytes", "type": "int64", "value": 249032 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7757c670662153b5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682578, + "duration": 26605, + "tags": [ + { "key": "blockID", "type": "string", "value": "99a8b127-bef6-4718-997b-18e5cb6bee81" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894709180, + "fields": [ + { "key": "bytes", "type": "int64", "value": 443904 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "033e809d9deb02fb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683129, + "duration": 26149, + "tags": [], + "logs": [ + { "timestamp": 1605873894683139, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709277, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0701e7633d141024", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683124, + "duration": 26160, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5a1fcbfa2c2e077e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682990, + "duration": 26296, + "tags": [], + "logs": [ + { "timestamp": 1605873894682993, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709285, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2639318a16168a94", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682987, + "duration": 26304, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "573267e2aab9eb37", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683831, + "duration": 25627, + "tags": [ + { "key": "blockID", "type": "string", "value": "9a5df823-d980-4671-b33f-ef92e485232f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894709455, + "fields": [ + { "key": "bytes", "type": "int64", "value": 357872 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3705123c90491605", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683886, + "duration": 25575, + "tags": [], + "logs": [ + { "timestamp": 1605873894683890, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709460, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46138581a74be710", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683883, + "duration": 25585, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "369cd4694f877602", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684325, + "duration": 25173, + "tags": [], + "logs": [ + { "timestamp": 1605873894684385, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709498, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7aab906468c79c5b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894703359, + "duration": 6145, + "tags": [], + "logs": [ + { "timestamp": 1605873894703375, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709503, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57f0ffddbcc40049", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684321, + "duration": 25185, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0c27a77ad2f6bbb3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894703354, + "duration": 6155, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "27f360a42e423410", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696933, + "duration": 12666, + "tags": [], + "logs": [ + { "timestamp": 1605873894696942, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709598, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7e5086a8bb3eb3b3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696926, + "duration": 12678, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42f4a2e45bc6b552", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683602, + "duration": 26169, + "tags": [], + "logs": [ + { "timestamp": 1605873894683608, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "693c3e7a4e085ce6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683598, + "duration": 26180, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7645427b1d8ca012", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894694874, + "duration": 15023, + "tags": [], + "logs": [ + { "timestamp": 1605873894694896, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894709897, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e6e130f1e7bf5ca", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894694866, + "duration": 15038, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2155087a44565c8a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700685, + "duration": 9446, + "tags": [], + "logs": [ + { "timestamp": 1605873894700698, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894710131, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66ed873b2793ee77", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700678, + "duration": 9459, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "69654d80ac69ec92", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702020, + "duration": 8202, + "tags": [], + "logs": [ + { "timestamp": 1605873894702031, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894710221, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3a0447242878ba00", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701338, + "duration": 8890, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e73b563bfa4df76", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683106, + "duration": 27358, + "tags": [ + { "key": "blockID", "type": "string", "value": "b89a056f-d8cd-41e9-84ad-445e68d0a0d5" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894710462, + "fields": [ + { "key": "bytes", "type": "int64", "value": 337664 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e958ff5d95860cf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683591, + "duration": 27357, + "tags": [ + { "key": "blockID", "type": "string", "value": "4767ecb2-01d3-450b-b005-6b9219fdfd71" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894710945, + "fields": [ + { "key": "bytes", "type": "int64", "value": 421576 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4854f2803a2439d0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684313, + "duration": 26669, + "tags": [ + { "key": "blockID", "type": "string", "value": "ec9c982f-485f-47b2-be74-a7f203368ede" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894710972, + "fields": [ + { "key": "bytes", "type": "int64", "value": 409016 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62aa1124fbaafe29", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684107, + "duration": 26934, + "tags": [], + "logs": [ + { "timestamp": 1605873894684113, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711040, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "67ee705c301e7e2a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684102, + "duration": 26945, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "417798c3fbab4244", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894696911, + "duration": 14252, + "tags": [ + { "key": "blockID", "type": "string", "value": "6a346739-04b1-4e86-8f87-e182b01cf5cd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894711160, + "fields": [ + { "key": "bytes", "type": "int64", "value": 407144 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "17faaf92fbea2ed9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683730, + "duration": 27707, + "tags": [], + "logs": [ + { "timestamp": 1605873894683736, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711435, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "29d4e2aa59eae59e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683726, + "duration": 27719, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3b9a85f6cd6075b8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701327, + "duration": 10230, + "tags": [ + { "key": "blockID", "type": "string", "value": "ece056d3-aa27-464b-81a8-643b3ae208e4" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894711554, + "fields": [ + { "key": "bytes", "type": "int64", "value": 359960 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0da2897c85659567", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683377, + "duration": 28594, + "tags": [], + "logs": [ + { "timestamp": 1605873894683384, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711971, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4407d391acba81fc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683373, + "duration": 28605, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1987773829521f8f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699105, + "duration": 12885, + "tags": [], + "logs": [ + { "timestamp": 1605873894699119, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894711989, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c9553a6471269c6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699099, + "duration": 12896, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3dedf220c1f51d38", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704853, + "duration": 7356, + "tags": [], + "logs": [ + { "timestamp": 1605873894704879, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712209, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25820f0eebf05ab3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704846, + "duration": 7370, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7027388faf7e1bf1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684937, + "duration": 27418, + "tags": [], + "logs": [ + { "timestamp": 1605873894684943, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712354, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "44c6d6c7e1afb67d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684933, + "duration": 27429, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3f654e75b41629f5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683718, + "duration": 28677, + "tags": [ + { "key": "blockID", "type": "string", "value": "0e5b1fbb-ab10-44b7-89a0-f8932ee26dcf" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712392, + "fields": [ + { "key": "bytes", "type": "int64", "value": 369000 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4fa1d1a031112ab0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682996, + "duration": 29565, + "tags": [], + "logs": [ + { "timestamp": 1605873894683006, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712560, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e0985a0b4168ff2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682988, + "duration": 29577, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7a7bf32e81f4317e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682984, + "duration": 29753, + "tags": [ + { "key": "blockID", "type": "string", "value": "c56f4809-bc48-4f81-9656-a3bbb96ba87e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712734, + "fields": [ + { "key": "bytes", "type": "int64", "value": 406296 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66d4f363dfa46bdb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684041, + "duration": 28730, + "tags": [], + "logs": [ + { "timestamp": 1605873894684111, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "616b800031f78e5f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684037, + "duration": 28741, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5b0d3da4dac0a4ab", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683364, + "duration": 29595, + "tags": [ + { "key": "blockID", "type": "string", "value": "10e42379-1c35-419e-a26c-2630b9d2cdd2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712956, + "fields": [ + { "key": "bytes", "type": "int64", "value": 354744 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1571e420dca57b9f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683878, + "duration": 29122, + "tags": [ + { "key": "blockID", "type": "string", "value": "adb287c7-69e4-4ed8-8604-c3302c766db2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894712996, + "fields": [ + { "key": "bytes", "type": "int64", "value": 300104 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3120fb610c52c9a6", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684415, + "duration": 28590, + "tags": [ + { "key": "blockID", "type": "string", "value": "faebcb3d-444a-4675-8e55-2f46dbcaa1d7" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713002, + "fields": [ + { "key": "bytes", "type": "int64", "value": 410672 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "46feff0edeabb674", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683135, + "duration": 29700, + "tags": [], + "logs": [ + { "timestamp": 1605873894683141, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894712834, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "32df737f09cd2bf9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683131, + "duration": 29904, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "604de25c9811a395", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699064, + "duration": 14141, + "tags": [ + { "key": "blockID", "type": "string", "value": "7df8ef23-0902-4b4b-92aa-b6c1aeb3c9c2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713203, + "fields": [ + { "key": "bytes", "type": "int64", "value": 436328 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7867c14538ff0c61", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684923, + "duration": 28333, + "tags": [ + { "key": "blockID", "type": "string", "value": "a94e5162-7e01-4bd6-b5c8-1bc3b50c67c6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713253, + "fields": [ + { "key": "bytes", "type": "int64", "value": 433064 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "04e793f4b075b20f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684557, + "duration": 28822, + "tags": [], + "logs": [ + { "timestamp": 1605873894684565, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713378, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3084a10a11a62355", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684553, + "duration": 28832, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0a0b86e5738d630b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685048, + "duration": 28355, + "tags": [ + { "key": "blockID", "type": "string", "value": "36ce4c95-0cb6-4803-bdfd-b316b3c0cc4c" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713400, + "fields": [ + { "key": "bytes", "type": "int64", "value": 293136 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62ea00c2c871a91e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704821, + "duration": 8702, + "tags": [ + { "key": "blockID", "type": "string", "value": "b9c0dc2b-ee12-4876-a517-2902c6fe655e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713521, + "fields": [ + { "key": "bytes", "type": "int64", "value": 415912 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0f54c2d4ac7df141", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683769, + "duration": 29774, + "tags": [], + "logs": [ + { "timestamp": 1605873894683775, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713542, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e650633f1c4cb45", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683764, + "duration": 29784, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4cff4ebd296d36f0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684028, + "duration": 29524, + "tags": [ + { "key": "blockID", "type": "string", "value": "e6200492-f24a-40ef-946a-e89170d1ac54" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894713549, + "fields": [ + { "key": "bytes", "type": "int64", "value": 447592 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0da82a874696fec5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684230, + "duration": 29351, + "tags": [], + "logs": [ + { "timestamp": 1605873894684236, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713581, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "20334815e0eb1b97", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684226, + "duration": 29362, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "20de4a897b30c066", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683315, + "duration": 30412, + "tags": [], + "logs": [ + { "timestamp": 1605873894683323, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713726, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6edcc31aa4c96617", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683309, + "duration": 30424, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3594272577366bc9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684491, + "duration": 29257, + "tags": [], + "logs": [ + { "timestamp": 1605873894684498, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713747, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01e9f897c4145c38", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684485, + "duration": 29270, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "53e0bef2bbb77bea", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684901, + "duration": 28911, + "tags": [], + "logs": [ + { "timestamp": 1605873894684908, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713812, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3df7804d8e682193", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684897, + "duration": 28919, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5664530667612f1f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682860, + "duration": 31015, + "tags": [], + "logs": [ + { "timestamp": 1605873894682871, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894713875, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4b6340b15001f8c8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682855, + "duration": 31026, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b3d3f0643735e5f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894694842, + "duration": 19190, + "tags": [ + { "key": "blockID", "type": "string", "value": "f0b87e56-00a6-4270-8ce0-b47affb9113e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714027, + "fields": [ + { "key": "bytes", "type": "int64", "value": 310320 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4a4b3e0d2f115bcf", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683987, + "duration": 30363, + "tags": [], + "logs": [ + { "timestamp": 1605873894683994, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714350, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1e0da3179b38449d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683983, + "duration": 30374, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "318fcd8e3bfc42c7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684209, + "duration": 30152, + "tags": [], + "logs": [ + { "timestamp": 1605873894684215, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714360, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "37e82ddc44e6bf60", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684205, + "duration": 30162, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0271272ae09aac5f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684542, + "duration": 29977, + "tags": [ + { "key": "blockID", "type": "string", "value": "bfbb9652-84f8-4145-8091-8197ea922ad3" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714514, + "fields": [ + { "key": "bytes", "type": "int64", "value": 432848 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2d80feb23cbbb7cd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684508, + "duration": 30161, + "tags": [], + "logs": [ + { "timestamp": 1605873894684515, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714668, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01ad9e5d3837c5b6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684503, + "duration": 30172, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42698e68a26de8cf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683756, + "duration": 30942, + "tags": [ + { "key": "blockID", "type": "string", "value": "55f71d63-05b0-4c3a-b79f-a2563307bf40" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714695, + "fields": [ + { "key": "bytes", "type": "int64", "value": 402624 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6ea302c343fec88f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683645, + "duration": 31247, + "tags": [], + "logs": [ + { "timestamp": 1605873894683652, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714891, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "279e17d93d4978da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683641, + "duration": 31258, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "59b29ac1ab225873", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683001, + "duration": 31930, + "tags": [], + "logs": [ + { "timestamp": 1605873894683006, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894714930, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4196b1f250632b3e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682998, + "duration": 31938, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "224b550ee6ad2bf2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684889, + "duration": 30088, + "tags": [ + { "key": "blockID", "type": "string", "value": "07baec6a-187a-493b-b160-772936b5a3f0" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894714974, + "fields": [ + { "key": "bytes", "type": "int64", "value": 429360 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "368bcd97b5e9dde0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684678, + "duration": 30934, + "tags": [], + "logs": [ + { "timestamp": 1605873894684685, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894715611, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3d88bddf112b8ae2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684672, + "duration": 30946, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "718a103bd19501b2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684196, + "duration": 31424, + "tags": [ + { "key": "blockID", "type": "string", "value": "a85669d8-148b-4d61-a359-8f97c036b880" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894715617, + "fields": [ + { "key": "bytes", "type": "int64", "value": 401280 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2b56997697dd91c0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684472, + "duration": 31151, + "tags": [ + { "key": "blockID", "type": "string", "value": "75911f2c-fc5e-4ef1-bcff-9abad2120f23" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894715621, + "fields": [ + { "key": "bytes", "type": "int64", "value": 397808 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c049cad7edf280e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683630, + "duration": 32119, + "tags": [ + { "key": "blockID", "type": "string", "value": "fdcc5380-c15f-41c2-9a34-623d6cdd2d5a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894715733, + "fields": [ + { "key": "bytes", "type": "int64", "value": 397464 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e3d16e8ed14d90c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702877, + "duration": 12975, + "tags": [], + "logs": [ + { "timestamp": 1605873894702894, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894715852, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7bd595782cdb70c3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699700, + "duration": 16154, + "tags": [], + "logs": [ + { "timestamp": 1605873894699708, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894715853, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "08a9d074d520a512", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702864, + "duration": 12993, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "083316368540b811", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699695, + "duration": 16164, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7f067cadc2b4569d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684535, + "duration": 31520, + "tags": [], + "logs": [ + { "timestamp": 1605873894684540, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716053, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5061bd596bc8a7e7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684531, + "duration": 31530, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4b9772650994e725", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682990, + "duration": 33209, + "tags": [ + { "key": "blockID", "type": "string", "value": "61022db6-4401-40b6-a3a2-1f4cd5ccb430" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716196, + "fields": [ + { "key": "bytes", "type": "int64", "value": 380504 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0e9c6b89215308ba", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683975, + "duration": 32340, + "tags": [ + { "key": "blockID", "type": "string", "value": "f17c848f-2f99-4215-a5e9-1f55d8e15c1e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716311, + "fields": [ + { "key": "bytes", "type": "int64", "value": 447736 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "030573bc0520e3c2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683061, + "duration": 33348, + "tags": [], + "logs": [ + { "timestamp": 1605873894683067, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716409, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0d2e16a8cf201e5a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683057, + "duration": 33357, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0361f359be22f9c8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702474, + "duration": 14135, + "tags": [], + "logs": [ + { "timestamp": 1605873894702483, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716607, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5310c5c355550cad", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702463, + "duration": 14156, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "16870d24920c25b8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699686, + "duration": 17072, + "tags": [ + { "key": "blockID", "type": "string", "value": "320a9ecd-a9fd-4c88-8aeb-e8a312dcce0c" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716753, + "fields": [ + { "key": "bytes", "type": "int64", "value": 424544 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62090e9e1c22bb56", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707676, + "duration": 9095, + "tags": [], + "logs": [ + { "timestamp": 1605873894707691, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716771, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "02d91deb1ff0ea76", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707670, + "duration": 9107, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0cc47cc1eb5deb29", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702844, + "duration": 13991, + "tags": [ + { "key": "blockID", "type": "string", "value": "04e21143-53ef-4083-948e-3bbe502c2d44" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716832, + "fields": [ + { "key": "bytes", "type": "int64", "value": 401728 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b27a749f4d1b557", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684297, + "duration": 32550, + "tags": [ + { "key": "blockID", "type": "string", "value": "d1ffbf86-0e11-4b6e-b9ae-8466e7c42a90" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894716844, + "fields": [ + { "key": "bytes", "type": "int64", "value": 193992 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3c5f2282a3e7c658", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683345, + "duration": 33584, + "tags": [], + "logs": [ + { "timestamp": 1605873894683352, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894716927, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6fc62f7a1ae1a6e9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683340, + "duration": 33596, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "524a9c941765266a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684584, + "duration": 32449, + "tags": [], + "logs": [ + { "timestamp": 1605873894684592, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894717030, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5bcaa6a4a1c06160", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684579, + "duration": 32460, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2d4e045a72c17ff2", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684465, + "duration": 32666, + "tags": [ + { "key": "blockID", "type": "string", "value": "4c9f58b7-b944-4692-9d5b-14270bd1b8d6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717127, + "fields": [ + { "key": "bytes", "type": "int64", "value": 436224 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25548a46750dfecb", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684652, + "duration": 32684, + "tags": [ + { "key": "blockID", "type": "string", "value": "ffd8fb66-db97-4451-9a97-bfb6631b82a5" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717332, + "fields": [ + { "key": "bytes", "type": "int64", "value": 434536 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5f4913a50dcd37c8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683050, + "duration": 34487, + "tags": [ + { "key": "blockID", "type": "string", "value": "0255db6b-061e-4ceb-9ae9-598588995be8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717533, + "fields": [ + { "key": "bytes", "type": "int64", "value": 392520 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6750f7ac4a5b50e8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684648, + "duration": 32974, + "tags": [], + "logs": [ + { "timestamp": 1605873894684654, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894717621, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "41f4b72bd0a14291", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684644, + "duration": 32984, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "03fac2f4c91b31b6", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702448, + "duration": 15285, + "tags": [ + { "key": "blockID", "type": "string", "value": "f7da9248-f02e-44df-b243-c1f5f69e0f67" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894717730, + "fields": [ + { "key": "bytes", "type": "int64", "value": 366872 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4934a16eeec96b0d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684005, + "duration": 33827, + "tags": [], + "logs": [ + { "timestamp": 1605873894684010, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894717831, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0ebf8034f9944320", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684001, + "duration": 33836, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0cc1f6dfcc153616", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683672, + "duration": 34388, + "tags": [], + "logs": [ + { "timestamp": 1605873894683679, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894718059, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "501e4211325ef503", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683667, + "duration": 34399, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7bcf0390730028ef", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683332, + "duration": 34943, + "tags": [ + { "key": "blockID", "type": "string", "value": "cbeb7cd1-c8b1-4290-be74-4caf7b3f2d69" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718272, + "fields": [ + { "key": "bytes", "type": "int64", "value": 432424 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1b30f12cd1728ebf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683995, + "duration": 34418, + "tags": [ + { "key": "blockID", "type": "string", "value": "2dd90b29-ffb5-4a27-bcd7-0950ca151c14" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718411, + "fields": [ + { "key": "bytes", "type": "int64", "value": 210280 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "22a3f914c23d3456", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684570, + "duration": 34106, + "tags": [ + { "key": "blockID", "type": "string", "value": "4fca87b1-ceb1-4290-a3cb-c0a970a7c5a6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718671, + "fields": [ + { "key": "bytes", "type": "int64", "value": 422528 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "169359b95c501fae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683035, + "duration": 35830, + "tags": [], + "logs": [ + { "timestamp": 1605873894683041, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894718864, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "71541fab4a38308f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683031, + "duration": 35841, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3a7fc15a2fb60753", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707659, + "duration": 11315, + "tags": [ + { "key": "blockID", "type": "string", "value": "57b3be9d-2234-4b8b-a380-424f30717e5b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894718970, + "fields": [ + { "key": "bytes", "type": "int64", "value": 437088 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "10e57e001b6c6127", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683210, + "duration": 35772, + "tags": [], + "logs": [ + { "timestamp": 1605873894683220, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894718980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2c3fb8ad983d67fc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683206, + "duration": 35784, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3fca8d21c0827061", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684366, + "duration": 34674, + "tags": [], + "logs": [ + { "timestamp": 1605873894684372, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894719039, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "25a226515c2f9150", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684362, + "duration": 34687, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5fb9111ac6a5d18d", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683274, + "duration": 35965, + "tags": [], + "logs": [ + { "timestamp": 1605873894683286, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894719238, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6945097dfeae216a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683268, + "duration": 35979, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6dd256468dea419f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684628, + "duration": 34986, + "tags": [ + { "key": "blockID", "type": "string", "value": "0a5b8e26-05d5-4df2-97c1-a57ccb631b5e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894719610, + "fields": [ + { "key": "bytes", "type": "int64", "value": 368392 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "22c3bb99916b1cf9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684954, + "duration": 34724, + "tags": [], + "logs": [ + { "timestamp": 1605873894684961, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894719678, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7786d37aacb34302", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684949, + "duration": 34735, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0b4489a19011e658", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702395, + "duration": 17621, + "tags": [], + "logs": [ + { "timestamp": 1605873894702404, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720015, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5fe309dbb10a8aa0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702385, + "duration": 17639, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "28a88c33b44009c0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684502, + "duration": 35545, + "tags": [], + "logs": [ + { "timestamp": 1605873894684508, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6d20c9fb0d7d023a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683045, + "duration": 37003, + "tags": [], + "logs": [ + { "timestamp": 1605873894683057, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7aed634e79451eff", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684353, + "duration": 35695, + "tags": [ + { "key": "blockID", "type": "string", "value": "c4b4a484-2704-49e8-926b-e7bb7a13c520" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720046, + "fields": [ + { "key": "bytes", "type": "int64", "value": 394640 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2361a627270177b2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684498, + "duration": 35556, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6365c636dea9cf69", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683040, + "duration": 37014, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7fd1af0b8e4b13e5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704467, + "duration": 15587, + "tags": [], + "logs": [ + { "timestamp": 1605873894704477, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720054, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4d0b05c2fe988374", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704458, + "duration": 15600, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "45d91fa92cb81841", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683191, + "duration": 36908, + "tags": [ + { "key": "blockID", "type": "string", "value": "abec5c1e-02b5-4165-8b3d-2940d3adb991" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720066, + "fields": [ + { "key": "bytes", "type": "int64", "value": 352392 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4287af315802d4cd", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704355, + "duration": 15863, + "tags": [], + "logs": [ + { "timestamp": 1605873894704369, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720218, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62b352c305041dcc", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704348, + "duration": 15876, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6ec165f264482f57", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683346, + "duration": 37139, + "tags": [], + "logs": [ + { "timestamp": 1605873894683351, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720484, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7400527184eeef19", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683342, + "duration": 37152, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=4ff7c150586c7e6f; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1561e391ecd756d5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684939, + "duration": 35696, + "tags": [ + { "key": "blockID", "type": "string", "value": "7dbea947-c624-454c-a99a-b2aa0c96c19f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720631, + "fields": [ + { "key": "bytes", "type": "int64", "value": 328592 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "57f916fcf19f117f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682643, + "duration": 38011, + "tags": [], + "logs": [ + { "timestamp": 1605873894682653, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720650, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1d4458304925bc0f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682638, + "duration": 38028, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=3ff0fd3a1cdb9b5e; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6f251bfe2c45ae12", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894709481, + "duration": 11241, + "tags": [], + "logs": [ + { "timestamp": 1605873894709489, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720722, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5365877f5f1070a3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894709476, + "duration": 11253, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=5d1a0e533881c649; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5472390246aac5c4", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683265, + "duration": 37540, + "tags": [ + { "key": "blockID", "type": "string", "value": "31cd1597-b435-467c-8726-9fd43cb8f75a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720802, + "fields": [ + { "key": "bytes", "type": "int64", "value": 263520 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "38b14977915ca22c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683471, + "duration": 37385, + "tags": [], + "logs": [ + { "timestamp": 1605873894683477, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894720855, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "08ecb88049158355", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683466, + "duration": 37394, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": ["invalid parent span IDs=241c721a4337e64b; skipping clock skew adjustment"] + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4cb042697154defa", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684490, + "duration": 36497, + "tags": [ + { "key": "blockID", "type": "string", "value": "37430ec1-eb84-4ad4-9bea-64b05bc05f0b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720984, + "fields": [ + { "key": "bytes", "type": "int64", "value": 329440 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01afdbfe975f8d6d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704258, + "duration": 16738, + "tags": [ + { "key": "blockID", "type": "string", "value": "52136585-3c3c-418c-85bb-079c46f30ee8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894720994, + "fields": [ + { "key": "bytes", "type": "int64", "value": 271408 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c881037e38b18ad", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894704334, + "duration": 16710, + "tags": [ + { "key": "blockID", "type": "string", "value": "293f4ca9-60cf-4dce-84f9-90d7a8903467" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721042, + "fields": [ + { "key": "bytes", "type": "int64", "value": 448208 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "727cf2a7b14f8891", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683209, + "duration": 37913, + "tags": [], + "logs": [ + { "timestamp": 1605873894683217, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894721121, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "72a3d0dd535ed714", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683196, + "duration": 37928, + "tags": [], + "logs": [ + { "timestamp": 1605873894683202, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894721124, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e28aae41ed950a0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683204, + "duration": 37923, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1034cca4b87566b9", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683192, + "duration": 37937, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5477c4334a555c1a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894702369, + "duration": 18817, + "tags": [ + { "key": "blockID", "type": "string", "value": "65c35f00-7bf4-4d7c-884e-57e1f4f386f1" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721184, + "fields": [ + { "key": "bytes", "type": "int64", "value": 343160 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "156254fce90fef6d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683261, + "duration": 38071, + "tags": [ + { "key": "blockID", "type": "string", "value": "5f6da848-1f43-4327-b791-c8607c834469" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721329, + "fields": [ + { "key": "bytes", "type": "int64", "value": 425008 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "18174ee576735b69", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683380, + "duration": 38087, + "tags": [], + "logs": [ + { "timestamp": 1605873894683386, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894721466, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3c984a418432da06", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683376, + "duration": 38097, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2cb4a90ec9e7ed56", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684207, + "duration": 37304, + "tags": [ + { "key": "blockID", "type": "string", "value": "da15aeab-47f3-4150-a3a0-0b899f5728e0" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721505, + "fields": [ + { "key": "bytes", "type": "int64", "value": 435448 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "31a678641a0daa14", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683185, + "duration": 38722, + "tags": [ + { "key": "blockID", "type": "string", "value": "7f859498-9292-4be9-9902-9cc0cc94db7f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894721902, + "fields": [ + { "key": "bytes", "type": "int64", "value": 382184 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "27ca437dde2b9612", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683192, + "duration": 38914, + "tags": [ + { "key": "blockID", "type": "string", "value": "723cdf42-e4bc-48dc-bda5-6173eb15dee4" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894722104, + "fields": [ + { "key": "bytes", "type": "int64", "value": 374272 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "777ba94dbf7e2679", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894703342, + "duration": 18950, + "tags": [ + { "key": "blockID", "type": "string", "value": "9ffb7568-b253-46bf-ae30-275a5370abdd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894722288, + "fields": [ + { "key": "bytes", "type": "int64", "value": 296800 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "447a1de4607678e0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894716862, + "duration": 5567, + "tags": [], + "logs": [ + { "timestamp": 1605873894716881, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722428, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4a1cb35fb165238f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894716854, + "duration": 5582, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "263d88ba7760646a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707662, + "duration": 14938, + "tags": [], + "logs": [ + { "timestamp": 1605873894707677, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722599, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7cb0a9332c646221", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707654, + "duration": 14951, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "178dbf7349f30deb", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682835, + "duration": 39846, + "tags": [ + { "key": "blockID", "type": "string", "value": "ae4b5b30-d87d-459f-8bfe-d05f4f169ced" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894722679, + "fields": [ + { "key": "bytes", "type": "int64", "value": 384344 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "150994409f1cb25a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700618, + "duration": 22118, + "tags": [], + "logs": [ + { "timestamp": 1605873894700633, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722736, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "585e5d65d550b215", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700613, + "duration": 22129, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "27511615066e34db", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684102, + "duration": 38879, + "tags": [], + "logs": [ + { "timestamp": 1605873894684108, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894722980, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "79b947d9ed7866a6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684097, + "duration": 38891, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6f2e6507fe12975c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894717161, + "duration": 5887, + "tags": [], + "logs": [ + { "timestamp": 1605873894717174, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723047, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "00cdeb6a7479c53f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894717155, + "duration": 5899, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "24bddd4ea3487e35", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683371, + "duration": 39804, + "tags": [ + { "key": "blockID", "type": "string", "value": "d7601ebc-c33c-492c-a1da-4aa053533084" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894723171, + "fields": [ + { "key": "bytes", "type": "int64", "value": 365144 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "53d7bccf2fc103c5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894716842, + "duration": 6390, + "tags": [ + { "key": "blockID", "type": "string", "value": "ca64f28a-77dc-4745-abdc-44054c1e5e40" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894723228, + "fields": [ + { "key": "bytes", "type": "int64", "value": 289352 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "552db884462abcc8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683762, + "duration": 39518, + "tags": [], + "logs": [ + { "timestamp": 1605873894683768, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723279, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "66bfd77009ceabee", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683756, + "duration": 39531, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "39ecc86ead7ef908", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684607, + "duration": 38759, + "tags": [], + "logs": [ + { "timestamp": 1605873894684613, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723365, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "23e63f9ee6638cc5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684602, + "duration": 38769, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3995f8a937161d18", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685603, + "duration": 37861, + "tags": [], + "logs": [ + { "timestamp": 1605873894685611, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723463, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7d8cbf547f13bab8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685598, + "duration": 37871, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "50ea9bcb501f096f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707639, + "duration": 15983, + "tags": [ + { "key": "blockID", "type": "string", "value": "7ba16a23-7c29-4c4e-a0a6-f5a35697f61e" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894723607, + "fields": [ + { "key": "bytes", "type": "int64", "value": 283976 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "36174ad72177e7e0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683065, + "duration": 40713, + "tags": [], + "logs": [ + { "timestamp": 1605873894683103, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723777, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6eb33f7265438984", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683062, + "duration": 40722, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0d173415b42b54df", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684351, + "duration": 39618, + "tags": [], + "logs": [ + { "timestamp": 1605873894684357, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894723968, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0353b10977450cf7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684346, + "duration": 39628, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7b094f4ebdce31c3", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894717141, + "duration": 6985, + "tags": [ + { "key": "blockID", "type": "string", "value": "f1a4a13f-5e5d-484e-8b53-1f4f8e1bad37" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724124, + "fields": [ + { "key": "bytes", "type": "int64", "value": 448472 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4bf98f62683b0e8e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894700602, + "duration": 23557, + "tags": [ + { "key": "blockID", "type": "string", "value": "2c8d9e08-28c9-43e0-a38e-48e205e70c0a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724156, + "fields": [ + { "key": "bytes", "type": "int64", "value": 454976 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3d32ea43a7ace6a3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685122, + "duration": 39094, + "tags": [], + "logs": [ + { "timestamp": 1605873894685153, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724214, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42780d9e0c2beb80", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685114, + "duration": 39108, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6d4dfd6622f9d4e5", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684594, + "duration": 39780, + "tags": [ + { "key": "blockID", "type": "string", "value": "6926ecb7-efff-41c5-ae95-85e0b8e75bed" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724372, + "fields": [ + { "key": "bytes", "type": "int64", "value": 364000 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "462c7cc77e9bde26", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684089, + "duration": 40384, + "tags": [ + { "key": "blockID", "type": "string", "value": "eacc5319-5c66-4ef0-bdc7-61ebcd665770" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894724469, + "fields": [ + { "key": "bytes", "type": "int64", "value": 375312 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "64c5e8cb8a8c9c87", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684553, + "duration": 40087, + "tags": [], + "logs": [ + { "timestamp": 1605873894684559, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724639, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a2abf3fa1e44a02", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684548, + "duration": 40098, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5185d47ca37c94b2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684289, + "duration": 40372, + "tags": [], + "logs": [ + { "timestamp": 1605873894684296, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724661, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "63f3f66dc1b96cb5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684283, + "duration": 40383, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "09dfedf04619fd00", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683527, + "duration": 41169, + "tags": [], + "logs": [ + { "timestamp": 1605873894683532, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724695, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4fcf6b895ba07eb1", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683524, + "duration": 41178, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1d3e7eff78cb43af", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684135, + "duration": 40664, + "tags": [], + "logs": [ + { "timestamp": 1605873894684142, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894724800, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "43860f9f193430f0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684131, + "duration": 40675, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7ea1f5a8b4dab8a7", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684338, + "duration": 40775, + "tags": [ + { "key": "blockID", "type": "string", "value": "2906f33b-d748-4827-8eb7-de90927a65dd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725108, + "fields": [ + { "key": "bytes", "type": "int64", "value": 431856 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "512c60978bd2eac4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721019, + "duration": 4095, + "tags": [], + "logs": [ + { "timestamp": 1605873894721028, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725112, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7e897df0e96d32b5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721010, + "duration": 4112, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "787b23c8fa301dd7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894706900, + "duration": 18234, + "tags": [], + "logs": [ + { "timestamp": 1605873894706927, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725133, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3ffd9ecc1161334a", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683055, + "duration": 42085, + "tags": [ + { "key": "blockID", "type": "string", "value": "26f1bad8-cd58-4801-8785-d52b8d833a90" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725137, + "fields": [ + { "key": "bytes", "type": "int64", "value": 414056 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2f86e3ff470976d3", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894706887, + "duration": 18254, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1a42c28bcd21acc8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685104, + "duration": 40144, + "tags": [ + { "key": "blockID", "type": "string", "value": "777d1eb8-cd33-44d5-8e34-2d253bd948a6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725246, + "fields": [ + { "key": "bytes", "type": "int64", "value": 407792 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "445f67ce7c86e918", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684540, + "duration": 40905, + "tags": [ + { "key": "blockID", "type": "string", "value": "d4b28adc-eb54-4e54-8639-40acbe82196a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725443, + "fields": [ + { "key": "bytes", "type": "int64", "value": 312232 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4dc9df94476d41ef", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713432, + "duration": 12062, + "tags": [], + "logs": [ + { "timestamp": 1605873894713448, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725366, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1f47248f05173a34", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713423, + "duration": 12078, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6ab47177b7f5e532", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684151, + "duration": 41357, + "tags": [], + "logs": [ + { "timestamp": 1605873894684157, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725507, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62088478a235ead5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684146, + "duration": 41368, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "021658e91c35b26e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683518, + "duration": 42121, + "tags": [ + { "key": "blockID", "type": "string", "value": "6deff928-b65a-437a-8d56-64d2397d9d1f" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725636, + "fields": [ + { "key": "bytes", "type": "int64", "value": 238936 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "48401abd95ffa153", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894712421, + "duration": 13359, + "tags": [], + "logs": [ + { "timestamp": 1605873894712431, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894725779, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a6bcedc4fc18a61", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894712416, + "duration": 13371, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "551f266c080ab0c6", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684122, + "duration": 41711, + "tags": [ + { "key": "blockID", "type": "string", "value": "5180765b-50b6-4de3-abab-ceea6086afb8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725830, + "fields": [ + { "key": "bytes", "type": "int64", "value": 393392 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "72970316c65770af", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894706872, + "duration": 18990, + "tags": [ + { "key": "blockID", "type": "string", "value": "541a6a31-d25a-4275-9688-228355c81085" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725860, + "fields": [ + { "key": "bytes", "type": "int64", "value": 280296 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "635a7471f4256c0b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684275, + "duration": 41695, + "tags": [ + { "key": "blockID", "type": "string", "value": "6bf57585-a03d-44e1-bd18-081b679d3e4a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894725967, + "fields": [ + { "key": "bytes", "type": "int64", "value": 434432 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7607fc837fc26251", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683802, + "duration": 42223, + "tags": [], + "logs": [ + { "timestamp": 1605873894683808, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726025, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "01269c3f9d50434a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683798, + "duration": 42233, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70114fe92b16120e", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721062, + "duration": 5033, + "tags": [], + "logs": [ + { "timestamp": 1605873894721068, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726093, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "37a69429cff69860", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721058, + "duration": 5043, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5262951e45efa67d", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894720997, + "duration": 5110, + "tags": [ + { "key": "blockID", "type": "string", "value": "5c09e7bd-2f9c-42d3-8d2f-863e93eaa939" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726103, + "fields": [ + { "key": "bytes", "type": "int64", "value": 246840 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1d6a76dd2ca6c3e6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894711590, + "duration": 14746, + "tags": [], + "logs": [ + { "timestamp": 1605873894711600, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726335, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7c75cd286737359f", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894711582, + "duration": 14759, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "78415d3812916d77", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707879, + "duration": 18497, + "tags": [], + "logs": [ + { "timestamp": 1605873894707887, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726375, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5f7ac8c4fd5c680a", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707874, + "duration": 18513, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b4bc2ee6a63726e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713413, + "duration": 13261, + "tags": [ + { "key": "blockID", "type": "string", "value": "b654e510-386a-470a-98c4-fb9833d21728" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726671, + "fields": [ + { "key": "bytes", "type": "int64", "value": 384056 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5f55f469fc2d6d29", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894712403, + "duration": 14367, + "tags": [ + { "key": "blockID", "type": "string", "value": "3fca3f89-a174-460d-9a87-92ed03555497" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726767, + "fields": [ + { "key": "bytes", "type": "int64", "value": 334896 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "19235cb1f2dccb32", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894711567, + "duration": 15276, + "tags": [ + { "key": "blockID", "type": "string", "value": "de228107-4ff6-449e-9f1f-6ab34765ab68" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894726841, + "fields": [ + { "key": "bytes", "type": "int64", "value": 225832 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "29b186beff361524", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699284, + "duration": 27562, + "tags": [], + "logs": [ + { "timestamp": 1605873894699298, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894726845, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0acce3e2af327bae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699278, + "duration": 27578, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b343c544d82bb3b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721050, + "duration": 5994, + "tags": [ + { "key": "blockID", "type": "string", "value": "459be32f-b91d-491b-aa61-5389b239eed8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727041, + "fields": [ + { "key": "bytes", "type": "int64", "value": 429792 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "62e3ccbe325de3d1", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683746, + "duration": 43412, + "tags": [ + { "key": "blockID", "type": "string", "value": "a1b8740a-6430-4113-93f4-ad9c52e42d62" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727155, + "fields": [ + { "key": "bytes", "type": "int64", "value": 366096 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "310178852fbf88e0", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682731, + "duration": 44481, + "tags": [], + "logs": [ + { "timestamp": 1605873894682741, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727211, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "42bb5e9919ea40f4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682726, + "duration": 44493, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "150c8cfeedab6a38", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894708915, + "duration": 18374, + "tags": [], + "logs": [ + { "timestamp": 1605873894708922, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727288, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1dda89d441503741", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894708910, + "duration": 18384, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6c1c11a742626433", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707863, + "duration": 19558, + "tags": [ + { "key": "blockID", "type": "string", "value": "ef68962f-224d-4b6c-9dcd-ef9be8607c72" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727419, + "fields": [ + { "key": "bytes", "type": "int64", "value": 423912 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4393a9fa65c8ceae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707853, + "duration": 19639, + "tags": [], + "logs": [ + { "timestamp": 1605873894707866, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727491, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1392dcdbb07bc781", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707848, + "duration": 19650, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0804f1e82c8828e2", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713028, + "duration": 14650, + "tags": [], + "logs": [ + { "timestamp": 1605873894713040, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894727677, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4e9e2ce15a6e596c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713023, + "duration": 14661, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0c763a5ef614a2f4", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685588, + "duration": 42409, + "tags": [ + { "key": "blockID", "type": "string", "value": "65fbe578-d535-4032-a12f-f249e7405363" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894727993, + "fields": [ + { "key": "bytes", "type": "int64", "value": 441088 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "31c1f2fd1bde2d49", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682863, + "duration": 45441, + "tags": [], + "logs": [ + { "timestamp": 1605873894682870, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728303, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a694924a7a45244", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682710, + "duration": 45639, + "tags": [ + { "key": "blockID", "type": "string", "value": "06a1301e-5b48-425e-a506-a4350afe3d0d" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728346, + "fields": [ + { "key": "bytes", "type": "int64", "value": 418168 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "73f7696fdccac589", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682859, + "duration": 45516, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4b52acf382a86008", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684137, + "duration": 44241, + "tags": [ + { "key": "blockID", "type": "string", "value": "57df43b4-8552-49e3-a1cd-f442609deaf2" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728373, + "fields": [ + { "key": "bytes", "type": "int64", "value": 425104 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "18688fac379c4a9e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894707836, + "duration": 20550, + "tags": [ + { "key": "blockID", "type": "string", "value": "c53e7db4-34cd-43d0-9383-de5e40936182" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728383, + "fields": [ + { "key": "bytes", "type": "int64", "value": 389928 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5fccf30d8fc010b8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894726139, + "duration": 2327, + "tags": [], + "logs": [ + { "timestamp": 1605873894726153, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728465, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1c2bf57fc386ac18", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894726130, + "duration": 2343, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7cfea87d3c8d06ca", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685457, + "duration": 43096, + "tags": [], + "logs": [ + { "timestamp": 1605873894685465, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728552, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "12a80c97c2a42f05", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685452, + "duration": 43107, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "08122694d5e37cb7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894723200, + "duration": 5455, + "tags": [], + "logs": [ + { "timestamp": 1605873894723209, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728654, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "313e6147867246d6", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894723194, + "duration": 5466, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2e4f73b3315eb612", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894715649, + "duration": 13049, + "tags": [], + "logs": [ + { "timestamp": 1605873894715658, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728698, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7e0457958022516b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894715644, + "duration": 13060, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "04db9d1320bc1720", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683309, + "duration": 45454, + "tags": [], + "logs": [ + { "timestamp": 1605873894683316, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894728762, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4702ca2057b120a0", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894726116, + "duration": 2652, + "tags": [ + { "key": "blockID", "type": "string", "value": "ec5b73d8-61f8-4210-8838-e6cfd253294b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728766, + "fields": [ + { "key": "bytes", "type": "int64", "value": 173264 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5e2c536cf48b42a8", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683305, + "duration": 45464, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "69bd1c5d5184626b", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894708900, + "duration": 19981, + "tags": [ + { "key": "blockID", "type": "string", "value": "de324468-b889-4f4c-af77-907353a719cd" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894728878, + "fields": [ + { "key": "bytes", "type": "int64", "value": 307032 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6849c669006c2759", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894714727, + "duration": 14480, + "tags": [], + "logs": [ + { "timestamp": 1605873894714736, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894729206, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6836249c56ca87ae", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894714722, + "duration": 14490, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4614a3c3374430a7", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894682850, + "duration": 46413, + "tags": [ + { "key": "blockID", "type": "string", "value": "ea416fc3-eedc-413e-9cc1-d8fd3548cfe6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729260, + "fields": [ + { "key": "bytes", "type": "int64", "value": 392160 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "33a5b0766e98269c", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894685441, + "duration": 44269, + "tags": [ + { "key": "blockID", "type": "string", "value": "b50c3305-8d80-42fc-88a4-97a8604c7066" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729705, + "fields": [ + { "key": "bytes", "type": "int64", "value": 346072 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "38da2ce44e352b40", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894722577, + "duration": 7233, + "tags": [], + "logs": [ + { "timestamp": 1605873894722588, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894729809, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2a0fc54146d07c21", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894722569, + "duration": 7247, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "398ed8e3573cf781", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894715632, + "duration": 14249, + "tags": [ + { "key": "blockID", "type": "string", "value": "2d9f964a-aac5-42e1-b410-866ad1706d7a" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729879, + "fields": [ + { "key": "bytes", "type": "int64", "value": 441600 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "47c93569ac9ecd04", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713013, + "duration": 16911, + "tags": [ + { "key": "blockID", "type": "string", "value": "96c1b791-af15-4554-a1bb-b0f200625856" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729919, + "fields": [ + { "key": "bytes", "type": "int64", "value": 446504 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3d61171be03f3db4", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894714709, + "duration": 15261, + "tags": [ + { "key": "blockID", "type": "string", "value": "ae957436-40cc-4d15-a3c3-26d10613aaf6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729967, + "fields": [ + { "key": "bytes", "type": "int64", "value": 287976 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3de17f2475734d79", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894723182, + "duration": 6796, + "tags": [ + { "key": "blockID", "type": "string", "value": "eb218dd8-99fa-4de7-87cd-8998b89e0778" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894729976, + "fields": [ + { "key": "bytes", "type": "int64", "value": 403400 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "712c995480f17232", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713402, + "duration": 16771, + "tags": [], + "logs": [ + { "timestamp": 1605873894713412, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730172, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "26950bc4bf84c34b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713392, + "duration": 16787, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "1bd3c2e5acbea9c4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701374, + "duration": 29019, + "tags": [], + "logs": [ + { "timestamp": 1605873894701383, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730393, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "2106e0853647ac08", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701369, + "duration": 29030, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6e59a327558a7329", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894699265, + "duration": 31183, + "tags": [ + { "key": "blockID", "type": "string", "value": "d8b5fee2-e2b3-445c-8ea9-243519a5c104" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894730443, + "fields": [ + { "key": "bytes", "type": "int64", "value": 400224 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "037acce8385b1858", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721017, + "duration": 9605, + "tags": [], + "logs": [ + { "timestamp": 1605873894721028, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730621, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3547a2504168d8c7", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721012, + "duration": 9617, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "0dc60016513590c8", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894722299, + "duration": 8583, + "tags": [ + { "key": "blockID", "type": "string", "value": "69120461-fbd8-4ca0-a5fb-957d745a22a8" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894730879, + "fields": [ + { "key": "bytes", "type": "int64", "value": 370472 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5d18bf1b78bd4e75", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684396, + "duration": 46590, + "tags": [], + "logs": [ + { "timestamp": 1605873894684402, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894730985, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3bdc5547104db28c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894684392, + "duration": 46600, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "682e6d017ce71dad", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894713268, + "duration": 17796, + "tags": [ + { "key": "blockID", "type": "string", "value": "84644a06-da08-4c3a-936f-70a634d8052d" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731057, + "fields": [ + { "key": "bytes", "type": "int64", "value": 353808 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3bbbdf937ccb2ccb", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721935, + "duration": 9258, + "tags": [], + "logs": [ + { "timestamp": 1605873894721944, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731193, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7c40bff5b749a532", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721928, + "duration": 9272, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "31f86900e7598e55", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894701358, + "duration": 29885, + "tags": [ + { "key": "blockID", "type": "string", "value": "186f31e1-409b-4c9c-95b5-abc662389d3b" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731240, + "fields": [ + { "key": "bytes", "type": "int64", "value": 434496 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "7471b3c5a188e8da", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894729995, + "duration": 1327, + "tags": [], + "logs": [ + { "timestamp": 1605873894730003, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731321, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "33cb5e55876f584c", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894729989, + "duration": 1338, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "58b7d0b41550e88f", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894729978, + "duration": 1396, + "tags": [ + { "key": "blockID", "type": "string", "value": "e0d8f1ac-a48f-4dea-868f-183e1a124fb5" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731373, + "fields": [ + { "key": "bytes", "type": "int64", "value": 16488 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70405f4198f01d16", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721544, + "duration": 9893, + "tags": [], + "logs": [ + { "timestamp": 1605873894721555, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731436, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "56f0f0d7120ea5a5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894721540, + "duration": 9903, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "69bb9bf8c37d9faf", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894683022, + "duration": 48646, + "tags": [ + { "key": "blockID", "type": "string", "value": "cf7747f5-68f9-490b-840d-9975c057c7e6" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894731663, + "fields": [ + { "key": "bytes", "type": "int64", "value": 425640 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "4ef61721dfd7f61b", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894720840, + "duration": 10841, + "tags": [], + "logs": [ + { "timestamp": 1605873894720869, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731680, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "70e8aa6d13e56007", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894720827, + "duration": 10860, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "297ff96c736c18f4", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894705333, + "duration": 26573, + "tags": [], + "logs": [ + { "timestamp": 1605873894705342, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894731904, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6a80209f067be4f5", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894705328, + "duration": 26585, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "5b3db530e83db855", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894731272, + "duration": 796, + "tags": [], + "logs": [ + { "timestamp": 1605873894731284, "fields": [{ "key": "keys requested", "type": "int64", "value": 1 }] }, + { "timestamp": 1605873894732067, "fields": [{ "key": "keys found", "type": "int64", "value": 1 }] } + ], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "6b2458a9486a4298", + "operationName": "Memcache.GetMulti", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894731265, + "duration": 886, + "tags": [ + { "key": "organization", "type": "string", "value": "1" }, + { "key": "span.kind", "type": "string", "value": "client" } + ], + "logs": [], + "processID": "p1", + "warnings": null + }, + { + "traceID": "3fa414edcef6ad90", + "spanID": "3139145bb422702e", + "operationName": "block.Find", + "references": [{ "refType": "CHILD_OF", "traceID": "3fa414edcef6ad90", "spanID": "1b26effbab24e95a" }], + "startTime": 1605873894731251, + "duration": 941, + "tags": [ + { "key": "blockID", "type": "string", "value": "f9fd03ba-91a8-476b-b809-d2b11bfa790d" }, + { "key": "shardKey", "type": "int64", "value": 5 } + ], + "logs": [ + { + "timestamp": 1605873894732190, + "fields": [ + { "key": "bytes", "type": "int64", "value": 8680 }, + { "key": "msg", "type": "string", "value": "bloom" } + ] + } + ], + "processID": "p1", + "warnings": null + } + ], + "processes": { + "p1": { + "serviceName": "s1" + } + }, + "warnings": null + } + ], + "total": 0, + "limit": 0, + "offset": 0, + "errors": null +} diff --git a/e2e-playwright/fixtures/tempo-response.json b/e2e-playwright/fixtures/tempo-response.json new file mode 100644 index 00000000000..30136a226a5 --- /dev/null +++ b/e2e-playwright/fixtures/tempo-response.json @@ -0,0 +1,1181 @@ +{ + "results": { + "A": { + "frames": [ + { + "schema": { + "name": "Trace", + "refId": "A", + "meta": { + "preferredVisualisationType": "trace" + }, + "fields": [ + { + "name": "traceID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "spanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "parentSpanID", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "operationName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceName", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "name": "serviceTags", + "type": "other", + "typeInfo": { + "frame": "other" + } + }, + { + "name": "startTime", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "duration", + "type": "number", + "typeInfo": { + "frame": "float64" + } + }, + { + "name": "logs", + "type": "other", + "typeInfo": { + "frame": "other" + } + }, + { + "name": "tags", + "type": "other", + "typeInfo": { + "frame": "other" + } + } + ] + }, + "data": { + "values": [ + [ + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599", + "04829080550953998599" + ], + [ + "4829080550953998599", + "2017565205134657253", + "8342878680821632593", + "1224460974534844837", + "275963539390240345", + "8319197348330196958", + "6652870949263121722", + "607757742690372462", + "4351457163250103362", + "6171925700166476032", + "7554371986156978275" + ], + [ + null, + "4829080550953998599", + "2017565205134657253", + "8342878680821632593", + "1224460974534844837", + "275963539390240345", + "4829080550953998599", + "6652870949263121722", + "607757742690372462", + "4351457163250103362", + "6171925700166476032" + ], + [ + "HTTP Client", + "HTTP POST", + "HTTP POST - post", + "HTTP Client", + "HTTP POST", + "HTTP POST - post", + "HTTP GET", + "HTTP GET - root", + "HTTP Client", + "HTTP GET", + "HTTP GET - root" + ], + ["lb", "lb", "app", "app", "app", "db", "lb", "app", "app", "app", "db"], + [ + [ + { + "value": "lb", + "key": "service.name" + }, + { + "value": "tns/loadgen", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "f55ab7e317a6", + "key": "host.name" + }, + { + "value": "172.24.0.8", + "key": "ip" + }, + { + "value": "36042b5ca9f81d60", + "key": "client-uuid" + } + ], + [ + { + "value": "lb", + "key": "service.name" + }, + { + "value": "tns/loadgen", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "f55ab7e317a6", + "key": "host.name" + }, + { + "value": "172.24.0.8", + "key": "ip" + }, + { + "value": "36042b5ca9f81d60", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "db", + "key": "service.name" + }, + { + "value": "tns/db", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "aae464791221", + "key": "host.name" + }, + { + "value": "172.24.0.2", + "key": "ip" + }, + { + "value": "18b1a6b5278dd643", + "key": "client-uuid" + } + ], + [ + { + "value": "lb", + "key": "service.name" + }, + { + "value": "tns/loadgen", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "f55ab7e317a6", + "key": "host.name" + }, + { + "value": "172.24.0.8", + "key": "ip" + }, + { + "value": "36042b5ca9f81d60", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "app", + "key": "service.name" + }, + { + "value": "tns/app", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "7945a05e75db", + "key": "host.name" + }, + { + "value": "172.24.0.7", + "key": "ip" + }, + { + "value": "78f645af1e60163d", + "key": "client-uuid" + } + ], + [ + { + "value": "db", + "key": "service.name" + }, + { + "value": "tns/db", + "key": "job" + }, + { + "value": "Jaeger-Go-2.22.1", + "key": "opencensus.exporterversion" + }, + { + "value": "aae464791221", + "key": "host.name" + }, + { + "value": "172.24.0.2", + "key": "ip" + }, + { + "value": "18b1a6b5278dd643", + "key": "client-uuid" + } + ] + ], + [ + 1620761543730.354, 1620761543730.3628, 1620761543732.044, 1620761543732.163, 1620761543732.191, + 1620761543733.925, 1620761543735.194, 1620761543738.197, 1620761543738.394, 1620761543738.398, + 1620761543744.6929 + ], + [17.826, 4.824, 2.588, 2.403, 2.401, 0.142, 13.266, 11.281, 8.593, 10.87, 0.136], + [ + null, + [ + { + "timestamp": 1620761543730.414, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543730.475, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "app", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543731.1738, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.7", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543731.184, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543731.6228, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543731.678, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543731.737, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543731.742, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543735.001, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543735.186, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null, + null, + [ + { + "timestamp": 1620761543732.209, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543732.236, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "db", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543733.196, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.2", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543733.203, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543733.459, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543733.549, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543733.613, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543733.618, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543734.241, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543734.592, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null, + [ + { + "timestamp": 1620761543735.23, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543735.277, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "app", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543736.9658, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.7", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543736.9758, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543737.705, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.7: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543737.773, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543737.8098, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543737.811, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543747.939, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543748.46, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null, + null, + [ + { + "timestamp": 1620761543738.446, + "fields": [ + { + "value": "GetConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543738.502, + "fields": [ + { + "value": "DNSStart", + "key": "event" + }, + { + "value": "db", + "key": "host" + } + ] + }, + { + "timestamp": 1620761543743.861, + "fields": [ + { + "value": "DNSDone", + "key": "event" + }, + { + "value": "172.24.0.2", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543743.918, + "fields": [ + { + "value": "ConnectStart", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543744.228, + "fields": [ + { + "value": "ConnectDone", + "key": "event" + }, + { + "value": "tcp", + "key": "network" + }, + { + "value": "172.24.0.2: 80", + "key": "addr" + } + ] + }, + { + "timestamp": 1620761543744.345, + "fields": [ + { + "value": "GotConn", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543744.414, + "fields": [ + { + "value": "WroteHeaders", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543744.416, + "fields": [ + { + "value": "WroteRequest", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543746.825, + "fields": [ + { + "value": "GotFirstResponseByte", + "key": "event" + } + ] + }, + { + "timestamp": 1620761543749.2668, + "fields": [ + { + "value": "ClosedBody", + "key": "event" + } + ] + } + ], + null + ], + [ + [ + { + "value": "const", + "key": "sampler.type" + }, + { + "value": true, + "key": "sampler.param" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 302, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "app: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 302, + "key": "http.status_code" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "/post", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 204, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "db: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 204, + "key": "http.status_code" + }, + { + "value": "POST", + "key": "http.method" + }, + { + "value": "/post", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "app: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "/", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "db: 80", + "key": "http.url" + }, + { + "value": false, + "key": "net/http.reused" + }, + { + "value": false, + "key": "net/http.was_idle" + }, + { + "value": "client", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ], + [ + { + "value": 200, + "key": "http.status_code" + }, + { + "value": "GET", + "key": "http.method" + }, + { + "value": "/", + "key": "http.url" + }, + { + "value": "net/http", + "key": "component" + }, + { + "value": "server", + "key": "span.kind" + }, + { + "value": 0, + "key": "status.code" + } + ] + ] + ] + } + } + ] + } + } +} diff --git a/e2e-playwright/panels-suite/dashlist.spec.ts b/e2e-playwright/panels-suite/dashlist.spec.ts new file mode 100644 index 00000000000..db4a8cccc9d --- /dev/null +++ b/e2e-playwright/panels-suite/dashlist.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables'; + +test.describe( + 'Panels test: DashList panel', + { + tag: '@panels', + }, + () => { + // this is to prevent the fix for https://github.com/grafana/grafana/issues/76800 from regressing + test('should pass current variable values correctly when `Include current template variable values` is set', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: PAGE_UNDER_TEST }); + + // check the initial value of the urls contain the variable value correctly + const panel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Include time range and variables enabled') + ); + await expect(panel).toBeVisible(); + + const links = panel.locator('a'); + const linkCount = await links.count(); + for (let i = 0; i < linkCount; i++) { + const href = await links.nth(i).getAttribute('href'); + expect(href).toContain('var-server=A'); + } + + // update variable to b + const serverVariable = dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('server')) + .locator('..') + .locator('input'); + await serverVariable.click(); + + await dashboardPage + .getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')) + .click(); + + // blur the dropdown + await page.locator('body').click({ position: { x: 0, y: 0 } }); + + // check the urls are updated with the new variable value + await expect(panel).toBeVisible(); + const updatedLinks = panel.locator('a'); + const updatedLinkCount = await updatedLinks.count(); + for (let i = 0; i < updatedLinkCount; i++) { + const href = await updatedLinks.nth(i).getAttribute('href'); + expect(href).toContain('var-server=B'); + } + }); + } +); diff --git a/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts new file mode 100644 index 00000000000..4cdb7afb081 --- /dev/null +++ b/e2e-playwright/panels-suite/frontend-sandbox-panel.spec.ts @@ -0,0 +1,154 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; + +const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; + +test.beforeAll(async ({ request }) => { + // Import the dashboard + await request.post('/api/dashboards/import', { + data: { + dashboard: panelSandboxDashboard, + folderUid: '', + overwrite: true, + inputs: [], + }, + }); +}); + +test.describe( + 'Panels test: Panel sandbox', + { + tag: ['@panels'], + }, + () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + await page.reload(); + }); + + test('Add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button adds iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeVisible(); + } + }); + + test('Reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeVisible(); + }); + + test('Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); + await expect(input).toHaveValue(''); + + await input.fill('x'); + await expect(input).toHaveValue('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeVisible(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + await page.reload(); + }); + + test('Does not add iframes to body', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button adds 3 iframes to the body + await page.locator('[data-testid="button-create-iframes"]').click(); + + const iframeIds = [ + 'createElementIframe', + 'innerHTMLIframe', + 'appendIframe', + 'prependIframe', + 'afterIframe', + 'beforeIframe', + 'outerHTMLIframe', + 'parseFromStringIframe', + 'insertBeforeIframe', + 'replaceChildIframe', + ]; + + for (const id of iframeIds) { + await expect(page.locator(`#${id}`)).toBeHidden(); + } + }); + + test('Does not reaches out of panel div', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + // this button reaches out of the panel div and modifies the element dataset + await page.locator('[data-testid="button-reach-out"]').click(); + await expect(page.locator('[data-sandbox-test="true"]')).toBeHidden(); + }); + + test('Does not Reaches out of the panel editor', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + const input = page.locator('[data-testid="panel-editor-custom-editor-input"]'); + await expect(input).toBeEnabled(); + + await input.fill('x'); + await expect(page.locator('[data-sandbox-test="panel-editor"]')).toBeHidden(); + }); + + test('Can access specific window global variables', async ({ page, gotoDashboardPage }) => { + await gotoDashboardPage({ + uid: DASHBOARD_ID, + }); + + await page.locator('[data-testid="button-test-globals"]').click(); + await expect(page.locator('[data-sandbox-global="Prism"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="jQuery"]')).toBeVisible(); + await expect(page.locator('[data-sandbox-global="location"]')).toBeVisible(); + }); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-layer-types.spec.ts b/e2e-playwright/panels-suite/geomap-layer-types.spec.ts new file mode 100644 index 00000000000..ba0d80d002e --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-layer-types.spec.ts @@ -0,0 +1,137 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +const MAP_LAYERS_TYPE = 'Map layers Layer type'; +const MAP_LAYERS_DATA = 'Map layers Data'; +const MAP_LAYERS_GEOJSON = 'Map layers GeoJSON URL'; + +test.describe( + 'Panels test: Geomap layer types', + { + tag: ['@panels'], + }, + () => { + test('Tests changing the layer type', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toBeVisible(); + const field = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE) + ); + await expect(field).toBeVisible(); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('markers'); + + // Heatmap + const input = field.locator('input'); + await input.fill('Heatmap'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('heatmap'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + + // GeoJSON + await input.fill('GeoJSON'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('geojson'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON)) + ).toBeVisible(); + + // Open Street Map + await input.fill('Open Street Map'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('osm-standard'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON)) + ).toBeHidden(); + + // CARTO basemap + await input.fill('CARTO basemap'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('carto'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme')) + ).toBeVisible(); + + // ArcGIS MapServer + await input.fill('ArcGIS MapServer'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('esri-xyz'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance') + ) + ).toBeVisible(); + + // XYZ Tile layer + await input.fill('XYZ Tile layer'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('xyz'); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution') + ) + ).toBeVisible(); + + // Night / Day (Alpha) + await input.fill('Night / Day'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('dayNight'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show')) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Night region color') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Display sun') + ) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Route (Alpha) + await input.fill('Route'); + await input.press('Enter'); + await expect(page.locator('[data-testid="layer-drag-drop-list"]')).toContainText('route'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Location Mode') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Map layers Style')) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-map-controls.spec.ts b/e2e-playwright/panels-suite/geomap-map-controls.spec.ts new file mode 100644 index 00000000000..de415dc8301 --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-map-controls.spec.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +test.describe( + 'Panels test: Geomap layer controls options', + { + tag: ['@panels'], + }, + () => { + test('Tests map controls options', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + // Wait until the query editor has been loaded by ensuring that the QueryEditor select contains the text 'flight_info_by_state.csv' + await expect( + page.locator(selectors.components.Select.singleValue('')).getByText('flight_info_by_state.csv') + ).toBeVisible(); + + const mapControlsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('Map controls') + ); + await expect(mapControlsGroup).toBeVisible(); + + // Show zoom field + const showZoomField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showZoomField); + await expect(showZoomField).toBeVisible(); + const zoomCheckbox = showZoomField.locator('input[type="checkbox"]'); + await zoomCheckbox.check({ force: true }); + await expect(zoomCheckbox).toBeChecked(); + + // Show attribution + const showAttributionField = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.showAttributionField + ); + await expect(showAttributionField).toBeVisible(); + const attributionCheckbox = showAttributionField.locator('input[type="checkbox"]'); + await attributionCheckbox.check({ force: true }); + await expect(attributionCheckbox).toBeChecked(); + + // Show scale + const showScaleField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showScaleField); + await expect(showScaleField).toBeVisible(); + const scaleCheckbox = showScaleField.locator('input[type="checkbox"]'); + await scaleCheckbox.check({ force: true }); + await expect(scaleCheckbox).toBeChecked(); + + // Show measure tool + const showMeasureField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showMeasureField); + await expect(showMeasureField).toBeVisible(); + const measureCheckbox = showMeasureField.locator('input[type="checkbox"]'); + await measureCheckbox.check({ force: true }); + await expect(measureCheckbox).toBeChecked(); + + // Show debug + const showDebugField = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.showDebugField); + await expect(showDebugField).toBeVisible(); + const debugCheckbox = showDebugField.locator('input[type="checkbox"]'); + await debugCheckbox.check({ force: true }); + await expect(debugCheckbox).toBeChecked(); + + const panelContent = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content); + await expect(panelContent).toBeVisible(); + + // Verify zoom + await expect(panelContent.locator('.ol-zoom')).toBeVisible(); + + // Verify attribution + await expect(panelContent.locator('.ol-attribution')).toBeVisible(); + + // Verify scale + await expect(panelContent.locator('.ol-scale-line')).toBeVisible(); + + // Verify measure tool + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.measureButton)).toBeVisible(); + + // Verify debug tool + await expect(dashboardPage.getByGrafanaSelector(selectors.components.DebugOverlay.wrapper)).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts new file mode 100644 index 00000000000..10df0887a0f --- /dev/null +++ b/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts @@ -0,0 +1,194 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'P2jR04WVk'; + +test.describe( + 'Panels test: Geomap spatial operations', + { + tag: ['@panels'], + }, + () => { + test('Tests location auto option', async ({ gotoDashboardPage, selectors }) => { + const dashboardPage = await gotoDashboardPage({ + uid: DASHBOARD_ID, + queryParams: new URLSearchParams({ editPanel: '1' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Auto'); + await locationInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Point')).toBeVisible(); + }); + + test('Tests location coords option', async ({ gotoDashboardPage, dashboardPage, selectors }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Coords'); + await locationInput.press('Enter'); + + const latitudeField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel + ); + await expect(latitudeField).toBeVisible(); + const latitudeInput = latitudeField.locator('input'); + await latitudeInput.fill('Lat'); + await latitudeInput.press('Enter'); + + const longitudeField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel + ); + await expect(longitudeField).toBeVisible(); + const longitudeInput = longitudeField.locator('input'); + await longitudeInput.fill('Lng'); + await longitudeInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Point')).toBeVisible(); + }); + + test('Tests geoshash field column appears in table view', async ({ + gotoDashboardPage, + dashboardPage, + selectors, + }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Geohash'); + await locationInput.press('Enter'); + + const geohashField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.geohash.geohashFieldLabel + ); + await expect(geohashField).toBeVisible(); + const geohashFieldInput = geohashField.locator('input'); + await geohashFieldInput.fill('State'); + await geohashFieldInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('State 1')).toBeVisible(); + }); + + test('Tests location lookup option', async ({ gotoDashboardPage, dashboardPage, selectors }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID, queryParams: new URLSearchParams({ editPanel: '1' }) }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.TransformTab.newTransform('Spatial operations')) + .click(); + + const actionLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.actionLabel + ); + const actionInput = actionLabel.locator('input'); + await actionInput.fill('Prepare spatial field'); + await actionInput.press('Enter'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.SpatialOperations.locationLabel) + ).toBeVisible(); + + const locationLabel = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.locationLabel + ); + const locationInput = locationLabel.locator('input'); + await locationInput.fill('Lookup'); + await locationInput.press('Enter'); + + const lookupField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel + ); + await expect(lookupField).toBeVisible(); + const lookupFieldInput = lookupField.locator('input'); + await lookupFieldInput.fill('State'); + await lookupFieldInput.press('Enter'); + + const gazetteerField = dashboardPage.getByGrafanaSelector( + selectors.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel + ); + await expect(gazetteerField).toBeVisible(); + const gazetteerFieldInput = gazetteerField.locator('input'); + await gazetteerFieldInput.fill('USA States'); + await gazetteerFieldInput.press('Enter'); + + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('Geometry')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/panelEdit_base.spec.ts b/e2e-playwright/panels-suite/panelEdit_base.spec.ts new file mode 100644 index 00000000000..66584d0387b --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_base.spec.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const PANEL_UNDER_TEST = 'Lines 500 data points'; + +test.describe( + 'Panels test: Panel edit base', + { + tag: ['@panels'], + }, + () => { + test('Tests various Panel edit scenarios', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'TkZXxlNG3' }); + + const panelTitle = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await expect(panelTitle).toBeVisible(); + + // Check that the panel is visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0); + await expect(panelTitle.locator('[data-testid="uplot-main-div"]').first()).toBeVisible(); + + // Open panel menu and click edit + await panelTitle.hover(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')).click(); + + // New panel editor opens when navigating from Panel menu + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Queries tab is rendered and open by default + const dataPane = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content); + await expect(dataPane).toBeVisible(); + + // Check that Queries tab is visible and active + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries'))).toBeVisible(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Queries1'); // there's already a query so therefore Query + 1 + + // Check query content is visible and other tabs are not + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden(); + + // Can change to Transform tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Transformations0'); // no transforms so Transform + 0 + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton) + ).toBeVisible(); + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.AlertTab.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelAlertTabContent.content)).toBeHidden(); + + // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Alert')).click(); + await expect(page.locator(selectors.components.Tab.active(''))).toContainText('Alert0'); // no alert so Alert + 0 + await expect(page.locator(`[data-testid="${selectors.components.QueryTab.content}"]`)).toBeHidden(); + + // Go back to Queries tab + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries')).click(); + + // Check that Time series is chosen + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Time series' + ); + + // Check that table view works + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.loadingBar(''))).toHaveCount(0); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + const tableHeader = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header); + await expect(tableHeader).toBeVisible(); + await expect(tableHeader.getByText('A-series')).toBeVisible(); + + // Change to Text panel + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Text')).click(); + // Check current visualization shows Text + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Text' + ); + + // Data pane should not be rendered + await expect(dataPane).toBeHidden(); + + // Change to Table panel + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Table')).click(); + // Check current visualization shows Table + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + 'Table' + ); + + // Data pane should be rendered + await expect(dataPane).toBeVisible(); + + // Field & Overrides tabs (need to switch to React based vis, i.e. Table) + await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleTableView).click({ force: true }); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header') + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldLabel('Table Column width') + ) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/panels-suite/panelEdit_queries.spec.ts b/e2e-playwright/panels-suite/panelEdit_queries.spec.ts new file mode 100644 index 00000000000..e60a7cc32b3 --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @@ -0,0 +1,118 @@ +import { Locator, Page } from 'playwright-core'; + +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +test.describe( + 'Panels test: Queries', + { + tag: ['@panels'], + }, + () => { + test('Tests various Panel edit queries scenarios', async ({ selectors, gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage({ + uid: '5SdHCadmz', + queryParams: new URLSearchParams({ editPanel: '3' }), + }); + + // New panel editor opens when navigating from Panel menu + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.General.content)).toBeVisible(); + + // Queries tab is rendered and open by default + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.DataPane.content)).toBeVisible(); + + // We expect row with refId A to exist and be visible + const initialRows = dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows); + await expect(initialRows).toHaveCount(1); + + // Add query button should be visible and clicking on it should create a new row + await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.addQuery).click(); + + // We expect row with refId A and B to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2); + + // Remove refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Remove query')) + .first() + .click(); + + // We expect row with refId B to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(1); + + // Duplicate refId B + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Duplicate query')) + .first() + .click(); + + // We expect row with refId B and A to exist and be visible + await expect(dashboardPage.getByGrafanaSelector(selectors.components.QueryEditorRows.rows)).toHaveCount(2); + + // Change to CSV Metric Values scenario for A + const scenarioSelect = dashboardPage + .getByGrafanaSelector(selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer) + .first(); + await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').first().click(); + await page.getByText('CSV Metric Values').first().click(); + + // Verify both queries are present in inspector + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + const resultIds = new Set([ + keyTexts[length - 2], // last 2 + keyTexts[length - 1], // last 2 + ]); + + expect(resultIds.has('A:')).toBe(true); + expect(resultIds.has('B:')).toBe(true); + }); + + // Hide response for row with refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response')) + .nth(1) + .click(); + + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + expect(keyTexts[length - 1]).toBe('B:'); + }); + + // Show response for row with refId A + await dashboardPage + .getByGrafanaSelector(selectors.components.QueryEditorRow.actionButton('Hide response')) + .nth(1) + .click(); + + await expectInspectorResultAndClose(page, dashboardPage, selectors, async (keys) => { + const keyTexts = await keys.allTextContents(); + const length = keyTexts.length; + const resultIds = new Set([ + keyTexts[length - 2], // last 2 + keyTexts[length - 1], // last 2 + ]); + + expect(resultIds.has('A:')).toBe(true); + expect(resultIds.has('B:')).toBe(true); + }); + }); + } +); + +const expectInspectorResultAndClose = async ( + page: Page, + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + expectCallback: (keys: Locator) => Promise +) => { + await dashboardPage.getByGrafanaSelector(selectors.components.QueryTab.queryInspectorButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.refreshButton).click(); + + const keys = page.locator(selectors.components.PanelInspector.Query.jsonObjectKeys('')); + await expect(keys.first()).toBeVisible(); + await expectCallback(keys); + + await dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close).click(); +}; diff --git a/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts b/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts new file mode 100644 index 00000000000..ed38aaedcbb --- /dev/null +++ b/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Panels test: Transformations', + { + tag: ['@panels'], + }, + () => { + test('Tests transformations editor', async ({ selectors, gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'TkZXxlNG3', + queryParams: new URLSearchParams({ editPanel: '47' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.calculationsLabel) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Transforms.Reduce.modeLabel)).toBeVisible(); + }); + + test('Tests case where transformations can be disabled and not clear out panel data', async ({ + selectors, + gotoDashboardPage, + }) => { + const dashboardPage = await gotoDashboardPage({ + uid: 'TkZXxlNG3', + queryParams: new URLSearchParams({ editPanel: '47' }), + }); + + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Transformations')).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.addTransformationButton).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.TransformTab.newTransform('Reduce')).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Transforms.disableTransformationButton).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts b/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts new file mode 100644 index 00000000000..b88dbbaa196 --- /dev/null +++ b/e2e-playwright/plugin-e2e/azuremonitor/azuremonitor.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'grafana-azure-monitor-datasource' }); + + await expect(await page.getByText('Type: Azure Monitor', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Authentication', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts b/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts new file mode 100644 index 00000000000..2932175da33 --- /dev/null +++ b/e2e-playwright/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'stackdriver' }); + + await expect(await page.getByText('Type: Google Cloud Monitoring', { exact: true })).toBeVisible(); + await expect(await page.getByText('Google JWT File', { exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts b/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts new file mode 100644 index 00000000000..c0f181560f0 --- /dev/null +++ b/e2e-playwright/plugin-e2e/cloudwatch/cloudwatch.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'cloudwatch' }); + + await expect(await page.getByText('Type: CloudWatch', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection Details', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts b/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts new file mode 100644 index 00000000000..41badfb29e3 --- /dev/null +++ b/e2e-playwright/plugin-e2e/elasticsearch/elasticsearch.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'elasticsearch' }); + + await expect(await page.getByText('Type: Elasticsearch', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts b/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts new file mode 100644 index 00000000000..a1c1a1f6fbd --- /dev/null +++ b/e2e-playwright/plugin-e2e/graphite/graphite.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'graphite' }); + + await expect(await page.getByText('Type: Graphite', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts b/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts new file mode 100644 index 00000000000..923106ebf0e --- /dev/null +++ b/e2e-playwright/plugin-e2e/influxdb/influxdb.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'influxdb' }); + + await expect(await page.getByText('Type: InfluxDB', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts b/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts new file mode 100644 index 00000000000..17373dcdce8 --- /dev/null +++ b/e2e-playwright/plugin-e2e/jaeger/jaeger.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'jaeger' }); + + await expect(await page.getByText('Type: Jaeger', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/loki/loki.spec.ts b/e2e-playwright/plugin-e2e/loki/loki.spec.ts similarity index 53% rename from e2e/plugin-e2e/loki/loki.spec.ts rename to e2e-playwright/plugin-e2e/loki/loki.spec.ts index 25c459140cc..4e7af47b071 100644 --- a/e2e/plugin-e2e/loki/loki.spec.ts +++ b/e2e-playwright/plugin-e2e/loki/loki.spec.ts @@ -3,6 +3,6 @@ import { test, expect } from '@grafana/plugin-e2e'; test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { await createDataSourceConfigPage({ type: 'loki' }); - await expect(await page.getByText('Type: Loki', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + await expect(page.getByText('Type: Loki', { exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); }); diff --git a/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts b/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts new file mode 100644 index 00000000000..7590562c548 --- /dev/null +++ b/e2e-playwright/plugin-e2e/mssql/mssql.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'mssql' }); + + await expect(await page.getByText('Type: Microsoft SQL Server', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/mysql/mocks/mysql.mocks.ts b/e2e-playwright/plugin-e2e/mysql/mocks/mysql.mocks.ts similarity index 100% rename from e2e/plugin-e2e/mysql/mocks/mysql.mocks.ts rename to e2e-playwright/plugin-e2e/mysql/mocks/mysql.mocks.ts diff --git a/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts b/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts new file mode 100644 index 00000000000..9bd0f85b7de --- /dev/null +++ b/e2e-playwright/plugin-e2e/mysql/mysql.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { tableNameWithSpecialCharacter } from './mocks/mysql.mocks'; +import { mockDataSourceRequest } from './utils'; + +test.beforeEach(mockDataSourceRequest); + +test( + 'code editor autocomplete should handle table name escaping/quoting', + { + tag: '@plugins', + }, + async ({ explorePage, selectors, page }) => { + await page.getByLabel('Code').check(); + + const editor = explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox'); + await editor.fill('S'); + await page.getByLabel('SELECT FROM ').locator('a').click(); + await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); + await page.keyboard.press('Enter'); + + await expect(editor).toHaveValue(`SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``); + + for (let i = 0; i < tableNameWithSpecialCharacter.length + 2; i++) { + await page.keyboard.press('Backspace'); + } + + await page.keyboard.press('Control+I'); + await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/mysql/utils.ts b/e2e-playwright/plugin-e2e/mysql/utils.ts similarity index 100% rename from e2e/plugin-e2e/mysql/utils.ts rename to e2e-playwright/plugin-e2e/mysql/utils.ts diff --git a/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts b/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts new file mode 100644 index 00000000000..9cf6c580927 --- /dev/null +++ b/e2e-playwright/plugin-e2e/mysql/visual-query-builder.spec.ts @@ -0,0 +1,176 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { test, expect } from '@grafana/plugin-e2e'; + +import { normalTableName } from './mocks/mysql.mocks'; +import { mockDataSourceRequest } from './utils'; + +test.beforeEach(mockDataSourceRequest); + +test.describe( + 'mysql', + { + tag: '@plugins', + }, + () => { + test('visual query builder should handle macros', async ({ explorePage, page }) => { + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); + await page.getByText(normalTableName, { exact: true }).click(); + + // Open Data operations + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click(); + const select = page.getByLabel('Select options menu'); + await select.locator(page.getByText('$__timeGroupAlias')).click(); + + // Open column selector + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) + .click(); + await select.locator(page.getByText('createdAt')).click(); + + // Open Interval selector + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval')) + .click(); + await select.locator(page.getByText('$__interval')).click(); + + await page.getByRole('button', { name: 'Add column' }).click(); + + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click(); + await select.locator(page.getByText('AVG')).click(); + + await explorePage + .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) + .nth(1) + .click(); + await select.locator(page.getByText('bigint')).click(); + + // Validate the query + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue( + `SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50` + ); + }); + + test('visual query builder should handle time filter macro', async ({ explorePage, page }) => { + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); + await page.getByText(normalTableName, { exact: true }).click(); + + // Open column selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectColumn).click(); + const select = page.getByLabel('Select options menu'); + await select.locator(page.getByText('createdAt')).click(); + + // Toggle where row + await page.getByLabel('Filter').last().click(); + + // Click add filter button + await page.getByRole('button', { name: 'Add filter' }).click(); + await page.getByRole('button', { name: 'Add filter' }).click(); // For some reason we need to click twice + + // Open field selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterField).click(); + await select.locator(page.getByText('createdAt')).click(); + + // Open operator selector + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); + await select.locator(page.getByText('Macros')).click(); + + // Open macros value selector + await explorePage.getByGrafanaSelector('Macros value selector').click(); + await select.locator(page.getByText('timeFilter', { exact: true })).click(); + + // Validate that the timeFilter macro was added + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue( + `SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n $__timeFilter(createdAt)\nLIMIT\n 50` + ); + + // Validate that the timeFilter macro was removed when changed to equals operator + await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); + await select.locator(page.getByText('==')).click(); + + await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).click(); + await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).blur(); + + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).not.toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n createdAt = NULL\nLIMIT\n 50`); + }); + + test('visual query builder should not crash when filter is set to select_any_in', async ({ explorePage, page }) => { + const queryParams = new URLSearchParams(); + queryParams.set('schemaVersion', '1'); + queryParams.set('orgId', '1'); + const panes = { + mmm: { + datasource: 'P4FDCC188E688367F', + queries: [ + { + refId: 'A', + datasource: { + type: 'mysql', + uid: 'P4FDCC188E688367F', + }, + format: 'table', + rawSql: "SELECT * FROM DataMaker.normalTable WHERE name IN ('a') LIMIT 50 ", + editorMode: 'builder', + sql: { + columns: [ + { + type: 'function', + parameters: [ + { + type: 'functionParameter', + name: '*', + }, + ], + }, + ], + groupBy: [ + { + type: 'groupBy', + property: { + type: 'string', + }, + }, + ], + limit: 50, + whereJsonTree: { + id: 'baa99aa9-0123-4456-b89a-b195d1dcfc6a', + type: 'group', + children1: [ + { + type: 'rule', + id: 'bb9a8bba-89ab-4cde-b012-3195d1dd2c91', + properties: { + fieldSrc: 'field', + field: 'name', + operator: 'select_any_in', + value: ['a'], + valueSrc: ['value'], + valueType: ['text'], + }, + }, + ], + }, + whereString: "name IN ('a')", + }, + dataset: 'DataMaker', + table: 'normalTable', + }, + ], + }, + }; + queryParams.set('panes', JSON.stringify(panes)); + + await explorePage.goto({ queryParams }); + + // Validate the query + await expect( + explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') + ).toHaveValue(`SELECT\n *\nFROM\n DataMaker.normalTable\nWHERE\n name IN ('a')\nLIMIT\n 50`); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts b/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts new file mode 100644 index 00000000000..1bab8050be5 --- /dev/null +++ b/e2e-playwright/plugin-e2e/opentsdb/opentsdb.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: decoupled frontend plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'opentsdb' }); + + await expect(await page.getByText('Type: OpenTSDB', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); + } +); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/README.md b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/README.md similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/README.md rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/README.md diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts new file mode 100644 index 00000000000..676de366071 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts @@ -0,0 +1,31 @@ +import * as e2e from '@grafana/e2e-selectors'; +import { expect, test } from '@grafana/plugin-e2e'; + +// let's disable the feature toggles for now, otherwise the getAlertRuleQueryRow fails and I don't see any other way to get the query row +test.use({ featureToggles: { alertingQueryAndExpressionsStepMode: false, alertingNotificationsStepMode: false } }); + +test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => { + test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({ + page, + alertRuleEditPage, + }) => { + await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule'); + + //add working query + const queryA = alertRuleEditPage.getAlertRuleQueryRow('A'); + await queryA.datasource.set('gdev-prometheus'); + await queryA.locator.getByLabel('Code').click(); + await page.waitForFunction(() => window.monaco); + await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); + await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))'); + + //add broken query + const newQuery = await alertRuleEditPage.clickAddQueryRow(); + await newQuery.datasource.set('gdev-prometheus'); + await newQuery.locator.getByLabel('Code').click(); + await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); + await page.keyboard.insertText('topk(5,'); + + await expect(alertRuleEditPage.evaluate()).not.toBeOK(); + }); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts similarity index 62% rename from e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts index 1a11fdd6a56..8ea363adab1 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/annotationEditPage.spec.ts @@ -41,13 +41,15 @@ const scenarios: Scenario[] = [ }, ]; -for (const scenario of scenarios) { - test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => { - annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status); - await annotationEditPage.datasource.set('gdev-testdata'); - await page.getByLabel('Scenario').last().fill('CSV Content'); - await page.keyboard.press('Tab'); - await annotationEditPage.runQuery(); - await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text }); - }); -} +test.describe('plugin-e2e-api-tests admin', { tag: ['@plugins'] }, () => { + for (const scenario of scenarios) { + test(`annotation query data with ${scenario.name}`, async ({ annotationEditPage, page }) => { + annotationEditPage.mockQueryDataResponse(scenario.mock, scenario.status); + await annotationEditPage.datasource.set('gdev-testdata'); + await page.getByLabel('Scenario').last().fill('CSV Content'); + await page.keyboard.press('Tab'); + await annotationEditPage.runQuery(); + await expect(annotationEditPage).toHaveAlert(scenario.severity, { hasText: scenario.text }); + }); + } +}); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts new file mode 100644 index 00000000000..6a1264d9c72 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.addPanel(); + await expect(page.url()).toContain('editPanel'); + }); + + test('add panel in new dashboard', async ({ dashboardPage, page }) => { + const panelEditPage = await dashboardPage.addPanel(); + await expect(panelEditPage.panel.locator).toBeVisible(); + await expect(page.url()).toContain('editPanel'); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts new file mode 100644 index 00000000000..3d6f2e924a4 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => { + test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected save data source config to fail when Prometheus server is not running') + ).not.toBeOK(); + }); + + test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => { + const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); + configPage.mockHealthCheckResponse({ status: 200 }); + await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); + await expect( + configPage.saveAndTest(), + formatExpectError('Expected data source config to be successfully saved') + ).toBeOK(); + }); + }); + + test.describe('test data source with frontend only health check', () => { + test('valid credentials should display a success alert on the page', async ({ + createDataSourceConfigPage, + page, + }) => { + const configPage = await createDataSourceConfigPage({ type: 'zipkin' }); + configPage.mockHealthCheckResponse({ message: 'Data source is working', status: 'OK' }, 200); + await page.getByPlaceholder('http://localhost:9411').fill('http://localhost:9411'); + await expect(configPage.saveAndTest()).toBeOK(); + await expect( + configPage, + formatExpectError('Expected data source config to display success alert after save') + ).toHaveAlert('success', { hasNotText: 'Datasource updated' }); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts new file mode 100644 index 00000000000..c97f2481fca --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('query data response should be OK when query is valid', async ({ explorePage }) => { + await explorePage.datasource.set('gdev-testdata'); + await expect( + explorePage.runQuery(), + formatExpectError('Expected Explore query to execute successfully') + ).toBeOK(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts new file mode 100644 index 00000000000..40771c4a7da --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1'; +const FALSY_CUSTOM_TOGGLE = 'custom_toggle2'; + +// override the feature toggles defined in playwright.config.ts only for tests in this file +test.use({ + featureToggles: { + [TRUTHY_CUSTOM_TOGGLE]: true, + [FALSY_CUSTOM_TOGGLE]: false, + }, +}); + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => { + expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy(); + expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy(); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts new file mode 100644 index 00000000000..177bc908af7 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts @@ -0,0 +1,95 @@ +import * as e2e from '@grafana/e2e-selectors'; +import { expect, test } from '@grafana/plugin-e2e'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('Loki editor', () => { + test('Autocomplete features should work as expected.', async ({ page }) => { + // Go to loki datasource in explore + await page.goto( + '/explore?schemaVersion=1&panes=%7B%22iap%22:%7B%22datasource%22:%22gdev-loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22gdev-loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1' + ); + + const queryEditor = page.getByTestId(e2e.selectors.components.QueryField.container); + const queryEditorRows = page.getByTestId('query-editor-rows'); + + async function assertQueryEditorEmpty() { + const queryEditorEmptyText = /^Enter to Rename.+/; + await expect(queryEditor).toHaveText(queryEditorEmptyText); + } + + async function clearInput() { + // Clear focused input + // Monaco appears to need some time to init keybindings after a change, adding this timeout to prevent flake + await page.waitForTimeout(100); + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + } + + // assert that the query builder is shown by default + await expect(page.getByText('Label filters')).toHaveCount(1); + + // switch to code editor + await page.getByLabel('Code').click(); + + await page.waitForFunction(() => window.monaco); + await expect(queryEditor).toHaveCount(1); + await assertQueryEditorEmpty(); + + // assert editor automatically adds close paren + await queryEditor.click(); + await page.keyboard.type('time('); + await expect(queryEditor).toContainText('time()'); + + // removes closing brace when opening brace is removed + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('avg_over_time('); + await expect(queryEditor).toContainText('avg_over_time()'); + await page.keyboard.press('Backspace'); + await expect(queryEditor).not.toContainText('avg_over_time()'); + await expect(queryEditor).toContainText('avg_over_time'); + + // keeps closing brace when opening brace is removed and inner values exist + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('time(test'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('Backspace'); + await expect(queryEditor).toContainText('timetest'); + + // overrides an automatically inserted paren + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('time()'); + await expect(queryEditor).toContainText('time()'); + + // does not override manually inserted braces + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('))'); + await expect(queryEditor).toContainText('))'); + + // Should execute the query when enter with shift is pressed + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.press('Shift+Enter'); + await expect(page.getByTestId('explore-no-data')).toHaveCount(1); + + // Suggestions plugin + await clearInput(); + await assertQueryEditorEmpty(); + await page.keyboard.type('av'); + await expect(queryEditorRows.getByLabel(/avg, docs:/)).toHaveCount(1); + await expect(queryEditorRows.getByLabel(/avg_over_time, docs:/)).toHaveCount(1); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts new file mode 100644 index 00000000000..0133a3e3712 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts @@ -0,0 +1,105 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('panel edit page', () => { + test('table panel data assertions with provisioned dashboard', async ({ gotoPanelEditPage }) => { + const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('table panel data assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Table'); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect( + panelEditPage.panel.data, + formatExpectError('Could not locate headers in table panel') + ).toContainText(['val1', 'val2', 'val3', 'val4']); + }); + + test('timeseries panel - table view assertions', async ({ panelEditPage }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization('Time series'); + await panelEditPage.refreshPanel(); + await panelEditPage.toggleTableView(); + await expect( + panelEditPage.panel.locator, + formatExpectError('Could not locate panel in panel edit page') + ).toBeVisible(); + await expect( + panelEditPage.panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['col1', 'col2']); + await expect( + panelEditPage.panel.data, + formatExpectError('Could not locate data elements in table panel') + ).toContainText(['val1', 'val2', 'val3', 'val4']); + }); + }); + + test.describe('dashboard page', () => { + test('getting panel by title', async ({ gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelByTitle('Colored background'); + await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + + test('getting panel by id', async ({ gotoDashboardPage }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.goto(); + const panel = await dashboardPage.getPanelByTitle('Colored background'); + await expect( + panel.fieldNames, + formatExpectError('Could not locate header elements in table panel') + ).toContainText(['Field', 'Max', 'Mean', 'Last']); + }); + }); + + test.describe('explore page', () => { + test('table panel', async ({ explorePage }) => { + const url = + 'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1'; + await explorePage.goto({ + queryParams: new URLSearchParams(url), + }); + await expect( + explorePage.timeSeriesPanel.locator, + formatExpectError('Could not locate time series panel in explore page') + ).toBeVisible(); + await expect( + explorePage.tablePanel.locator, + formatExpectError('Could not locate table panel in explore page') + ).toBeVisible(); + await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']); + }); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts new file mode 100644 index 00000000000..4772a9717e8 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -0,0 +1,214 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { successfulDataQuery } from '../mocks/queries'; +import { scenarios } from '../mocks/resources'; + +const PANEL_TITLE = 'Table panel E2E test'; +const TABLE_VIZ_NAME = 'Table'; +const TIME_SERIES_VIZ_NAME = 'Time series'; +const STANDARD_OTIONS_CATEGORY = 'Standard options'; +const DISPLAY_NAME_LABEL = 'Display name'; +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test.describe('query editor query data', () => { + test('query data response should be OK when query is valid', async ({ panelEditPage }) => { + await panelEditPage.datasource.set('gdev-testdata'); + await expect( + panelEditPage.refreshPanel(), + formatExpectError('Expected panel query to execute successfully') + ).toBeOK(); + }); + + test('query data response should not be OK and panel error should be displayed when query is invalid', async ({ + panelEditPage, + }) => { + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); + await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Expected panel error to be displayed after query execution') + ).toBeVisible(); + }); + }); + + test.describe('query editor with mocked responses', () => { + test('and resource `scenarios` is mocked', async ({ selectors, dashboardPage }) => { + await dashboardPage.mockResourceResponse('scenarios', scenarios); + const panelEditPage = await dashboardPage.addPanel(); + await panelEditPage.datasource.set('gdev-testdata'); + const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); + await queryEditorRow.getByLabel('Scenario').last().click(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Select.option), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText(scenarios.map((s) => s.name)); + }); + + test('mocked query data response', async ({ panelEditPage, selectors }) => { + await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); + await panelEditPage.datasource.set('gdev-testdata'); + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await panelEditPage.refreshPanel(); + await expect( + panelEditPage.panel.getErrorIcon(), + formatExpectError('Did not expect panel error to be displayed after query execution') + ).toBeHidden(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body), + formatExpectError('Expected certain select options to be displayed after clicking on the select input') + ).toHaveText('val1val2val3val4'); + }); + }); + + test.describe('edit panel plugin settings', () => { + test('change viz to table panel, set panel title and collapse section', async ({ + panelEditPage, + selectors, + page, + }) => { + await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker), + formatExpectError('Expected panel visualization to be set to table') + ).toHaveText(TABLE_VIZ_NAME); + await panelEditPage.setPanelTitle(PANEL_TITLE); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_TITLE)), + formatExpectError('Expected panel title to be updated') + ).toBeVisible(); + await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY); + await expect( + page.getByText(DISPLAY_NAME_LABEL), + formatExpectError('Expected section to be collapsed') + ).toBeVisible(); + }); + + test('Select time zone in timezone picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = await panelEditPage.getCustomOptions('Axis'); + const timeZonePicker = axisOptions.getSelect('Time zone'); + + await timeZonePicker.selectOption('Europe/Stockholm'); + await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); + }); + + test('select unit in unit picker', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const unitPicker = standardOptions.getUnitPicker('Unit'); + + await unitPicker.selectOption('Misc > Pixels'); + + await expect(unitPicker).toHaveSelected('Pixels'); + }); + + test('enter value in number input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const lineWith = axisOptions.getNumberInput('Soft min'); + + await lineWith.fill('10'); + + await expect(lineWith).toHaveValue('10'); + }); + + test('enter value in slider', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const graphOptions = panelEditPage.getCustomOptions('Graph styles'); + const lineWidth = graphOptions.getSliderInput('Line width'); + + await lineWidth.fill('10'); + + await expect(lineWidth).toHaveValue('10'); + }); + + test('select value in single value select', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const standardOptions = panelEditPage.getStandardOptions(); + const colorSchemeSelect = standardOptions.getSelect('Color scheme'); + + await colorSchemeSelect.selectOption('Classic palette'); + await expect(colorSchemeSelect).toHaveSelected('Classic palette'); + }); + + test('clear input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const title = panelOptions.getTextInput('Title'); + + await expect(title).toHaveValue('New panel'); + await title.clear(); + await expect(title).toHaveValue(''); + }); + + test('enter value in input', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const panelOptions = panelEditPage.getPanelOptions(); + const description = panelOptions.getTextInput('Description'); + + await expect(description).toHaveValue(''); + await description.fill('This is a panel'); + await expect(description).toHaveValue('This is a panel'); + }); + + test('unchecking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + + await showBorder.uncheck(); + await expect(showBorder).toBeChecked({ checked: false }); + }); + + test('checking switch', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const showBorder = axisOptions.getSwitch('Show border'); + + await expect(showBorder).toBeChecked({ checked: false }); + await showBorder.check(); + await expect(showBorder).toBeChecked(); + }); + + test('re-selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + + await placement.check('Auto'); + await expect(placement).toHaveChecked('Auto'); + }); + + test('selecting value in radio button group', async ({ panelEditPage }) => { + await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + const axisOptions = panelEditPage.getCustomOptions('Axis'); + const placement = axisOptions.getRadioGroup('Placement'); + + await placement.check('Right'); + await expect(placement).toHaveChecked('Right'); + }); + }); + + test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { + const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); + await panelEditPage.backToDashboard(); + await expect(page.url()).not.toContain('editPanel'); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts new file mode 100644 index 00000000000..c6025ca85fc --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +import { formatExpectError } from '../errors'; +import { prometheusLabels } from '../mocks/resources'; + +test.describe( + 'plugin-e2e-api-tests admin', + { + tag: ['@plugins'], + }, + () => { + test('variable query with mocked response', async ({ variableEditPage, page }) => { + variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); + variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); + await variableEditPage.datasource.set('gdev-prometheus'); + await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); + await page.keyboard.press('Tab'); + await variableEditPage.runQuery(); + await expect( + variableEditPage, + formatExpectError('Expected variable edit page to display certain label names after query execution') + ).toDisplayPreviews(prometheusLabels.data); + }); + } +); diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts new file mode 100644 index 00000000000..3deaf6341f7 --- /dev/null +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +test.describe( + 'plugin-e2e-api-test viewer', + { + tag: ['@plugins'], + }, + () => { + test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { + await page.goto('/'); + const homePageURL = new URL(page.url()); + await page.goto('/datasources', { waitUntil: 'networkidle' }); + const redirectedPageURL = new URL(page.url()); + expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); + }); + + test('current user should have viewer role', async ({ page, request }) => { + await page.goto('/'); + const response = await request.get('/api/user/orgs'); + await expect(response).toBeOK(); + await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' })); + }); + } +); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/errors.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/errors.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/errors.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/queries.ts diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts similarity index 100% rename from e2e/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts rename to e2e-playwright/plugin-e2e/plugin-e2e-api-tests/mocks/resources.ts diff --git a/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts b/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts new file mode 100644 index 00000000000..f898c39fd97 --- /dev/null +++ b/e2e-playwright/plugin-e2e/postgres/postgres.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'grafana-postgresql-datasource' }); + + await expect(await page.getByText('Type: PostgreSQL', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts b/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts new file mode 100644 index 00000000000..c2ec230b481 --- /dev/null +++ b/e2e-playwright/plugin-e2e/zipkin/zipkin.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Smoke test: plugin loads', + { + tag: '@plugins', + }, + async ({ createDataSourceConfigPage, page }) => { + await createDataSourceConfigPage({ type: 'zipkin' }); + + await expect(await page.getByText('Type: Zipkin', { exact: true })).toBeVisible(); + await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); + } +); diff --git a/e2e-playwright/scenarios/login.spec.ts b/e2e-playwright/scenarios/login.spec.ts new file mode 100644 index 00000000000..afef3d867d1 --- /dev/null +++ b/e2e-playwright/scenarios/login.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Scenario test: Can login successfully', + { + tag: ['@scenarios'], + }, + async ({ selectors, page }) => { + await page.goto(selectors.pages.Login.url); + + await page.getByTestId(selectors.pages.Login.username).fill('admin'); + await page.getByTestId(selectors.pages.Login.password).fill('admin'); + await page.getByTestId(selectors.pages.Login.submit).click(); + + await page.getByTestId(selectors.pages.Login.skip).click(); + + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + } +); diff --git a/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts b/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts new file mode 100644 index 00000000000..a6bea288915 --- /dev/null +++ b/e2e-playwright/smoke-tests-suite/1-smoketests.spec.ts @@ -0,0 +1,54 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Smoke tests', + { + tag: ['@smoke'], + }, + () => { + test('Login, create test data source, create dashboard and panel scenario', async ({ + createDataSourceConfigPage, + gotoDashboardPage, + selectors, + page, + }) => { + const dataSourceConfigPage = await createDataSourceConfigPage({ + name: `e2e-${uuidv4()}`, + type: 'grafana-testdata-datasource', + }); + const { datasource } = dataSourceConfigPage; + await dataSourceConfigPage.saveAndTest({ + path: `/api/datasources/uid/${datasource.uid}?accesscontrol=true`, + }); + + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Select CSV Metric Values scenario + const scenarioSelect = dashboardPage.getByGrafanaSelector( + selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer + ); + await expect(scenarioSelect).toBeVisible(); + await scenarioSelect.locator('input[id*="test-data-scenario-select-"]').click(); + await page.getByText('CSV Metric Values').click(); + + // Verify the graph renders by checking legend + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + + // Verify panel is added to dashboard + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.VizLegend.seriesName('A-series')) + ).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts b/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts new file mode 100644 index 00000000000..61e17bec7c6 --- /dev/null +++ b/e2e-playwright/smoke-tests-suite/panels-smokescreen.spec.ts @@ -0,0 +1,49 @@ +import { test, expect } from '@grafana/plugin-e2e'; +import { GrafanaBootConfig } from '@grafana/runtime'; + +test.describe( + 'Panels smokescreen', + { + tag: ['@smoke'], + }, + () => { + test('Tests each panel type in the panel edit view to ensure no crash', async ({ + gotoDashboardPage, + selectors, + page, + }) => { + // this test can absolutely take longer than the default 30s timeout + test.setTimeout(60000); + // Create new dashboard + const dashboardPage = await gotoDashboardPage({}); + + // Add new panel + await dashboardPage.addPanel(); + + // Get panel types from window object + const panelTypes = await page.evaluate(() => { + // @grafana/plugin-e2e doesn't export the full bootdata config + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const win = window as typeof window & { grafanaBootData: GrafanaBootConfig['bootData'] }; + return win.grafanaBootData?.settings?.panels ?? {}; + }); + + // Loop through every panel type and ensure no crash + for (const [_, panel] of Object.entries(panelTypes)) { + // Skip hidden and deprecated panels + if (!panel.hideFromList && panel.state !== 'deprecated') { + // Open visualization picker + const vizPicker = dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker); + await vizPicker.click(); + await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click(); + + // Verify panel type is selected + await expect(vizPicker).toHaveText(panel.name); + + // Ensure no unexpected error occurred + await expect(page.getByText('An unexpected error happened')).toBeHidden(); + } + } + }); + } +); diff --git a/e2e/plugin-e2e/start-and-run-suite b/e2e-playwright/start-and-run-suite similarity index 100% rename from e2e/plugin-e2e/start-and-run-suite rename to e2e-playwright/start-and-run-suite diff --git a/e2e-playwright/storybook/verify.spec.ts b/e2e-playwright/storybook/verify.spec.ts new file mode 100644 index 00000000000..1e24b83532d --- /dev/null +++ b/e2e-playwright/storybook/verify.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@grafana/plugin-e2e'; +// very basic test to verify that the button story loads correctly +// this is only intended to catch some basic build errors with storybook +test.describe( + 'Verify storybook', + { + tag: ['@storybook'], + }, + () => { + test('Loads the button story correctly', async ({ page }) => { + await page.goto('?path=/story/buttons-button--basic'); + const iframe = page.locator('#storybook-preview-iframe'); + await expect(iframe).toBeVisible(); + const iframeBody = iframe.contentFrame(); + await expect(iframeBody.getByText('Example button')).toBeVisible(); + }); + } +); diff --git a/e2e/test-plugins/README.md b/e2e-playwright/test-plugins/README.md similarity index 100% rename from e2e/test-plugins/README.md rename to e2e-playwright/test-plugins/README.md diff --git a/e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-app-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-app-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-app-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-app-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-app-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-app-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-app-test/plugin.json diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-datasource-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-datasource-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-datasource-test/plugin.json diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/img/logo.svg similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/img/logo.svg rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/img/logo.svg diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/module.js b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/module.js similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/module.js rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/module.js diff --git a/e2e/test-plugins/frontend-sandbox-panel-test/plugin.json b/e2e-playwright/test-plugins/frontend-sandbox-panel-test/plugin.json similarity index 100% rename from e2e/test-plugins/frontend-sandbox-panel-test/plugin.json rename to e2e-playwright/test-plugins/frontend-sandbox-panel-test/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/.gitignore b/e2e-playwright/test-plugins/grafana-extensionstest-app/.gitignore similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/.gitignore rename to e2e-playwright/test-plugins/grafana-extensionstest-app/.gitignore diff --git a/e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md b/e2e-playwright/test-plugins/grafana-extensionstest-app/CHANGELOG.md similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/CHANGELOG.md rename to e2e-playwright/test-plugins/grafana-extensionstest-app/CHANGELOG.md diff --git a/e2e/test-plugins/grafana-extensionstest-app/README.md b/e2e-playwright/test-plugins/grafana-extensionstest-app/README.md similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/README.md rename to e2e-playwright/test-plugins/grafana-extensionstest-app/README.md diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/ActionButton.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/ActionButton/index.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/QueryModal.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/components/QueryModal/index.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/constants.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/constants.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/constants.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/package.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/package.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedComponents.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/AddedLinks.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/ExposedComponents.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/pages/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/pages/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample1-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/components/App/index.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample2-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/AddedLinks.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/components/App/App.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/img/logo.svg diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/module.tsx diff --git a/e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/plugins/grafana-extensionexample3-app/plugin.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/testIds.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/testIds.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/testIds.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/testIds.ts diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts new file mode 100644 index 00000000000..9f3d3f0bf35 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts @@ -0,0 +1,16 @@ +import { test, expect } from '@grafana/plugin-e2e'; +import { testIds } from '../testIds'; +import pluginJson from '../plugin.json'; + +test.describe( + 'grafana-extensionstest-app', + { + tag: ['@plugins'], + }, + () => { + test('should display component exposed by another app', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/exposed-components`); + await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts new file mode 100644 index 00000000000..cb2039f6480 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import { testIds } from '../testIds'; + +test.describe( + 'grafana-extensionstest-app', + { + tag: ['@plugins'], + }, + () => { + test('should render component with usePluginComponents hook', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-components`); + await expect( + page.getByTestId(testIds.addedComponentsPage.container).getByTestId(testIds.appB.reusableAddedComponent) + ).toHaveText('Hello World!'); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts new file mode 100644 index 00000000000..852fe00fa85 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import pluginJson from '../plugin.json'; +import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json'; +import { testIds } from '../testIds'; + +test.describe('grafana-extensionstest-app', { tag: ['@plugins'] }, () => { + test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Go to A').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); + + test('should extend main app with link extension from app B', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Open from B').click(); + await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); + }); + + test('should extend main app with basic link extension from app A', async ({ page }) => { + await page.goto(`/a/${pluginJson.id}/added-links`); + const section = await page.getByTestId(testIds.addedLinksPage.section1); + await section.getByTestId(testIds.actions.button).click(); + await page.getByTestId(testIds.container).getByText('Basic link').click(); + await page.getByTestId(testIds.modal.open).click(); + await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); + }); + + test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({ + page, + }) => { + await page.goto(`/a/${testApp3pluginJson.id}`); + const container = await page.getByTestId(testIds.appC.section1); + await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible(); + }); + + const panelTitle = 'Link with defaults'; + const extensionTitle = 'Open from time series...'; + + const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; + const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; + + test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible(); + }); + + test('should add link extension (onclick) with defaults to time series panel', async ({ + gotoDashboardPage, + page, + }) => { + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); + }); + + test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { + const panelTitle = 'Link with new name'; + const extensionTitle = 'Open from piechart'; + const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); + const panel = await dashboardPage.getPanelByTitle(panelTitle); + await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); + await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); + }); +}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/tests/utils.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/tests/utils.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/tests/utils.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/tsconfig.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/tsconfig.json similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/tsconfig.json rename to e2e-playwright/test-plugins/grafana-extensionstest-app/tsconfig.json diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.routing.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/utils/utils.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/utils/utils.ts diff --git a/e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts b/e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts similarity index 100% rename from e2e/test-plugins/grafana-extensionstest-app/webpack.config.ts rename to e2e-playwright/test-plugins/grafana-extensionstest-app/webpack.config.ts diff --git a/e2e/test-plugins/grafana-test-datasource/CHANGELOG.md b/e2e-playwright/test-plugins/grafana-test-datasource/CHANGELOG.md similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/CHANGELOG.md rename to e2e-playwright/test-plugins/grafana-test-datasource/CHANGELOG.md diff --git a/e2e/test-plugins/grafana-test-datasource/README.md b/e2e-playwright/test-plugins/grafana-test-datasource/README.md similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/README.md rename to e2e-playwright/test-plugins/grafana-test-datasource/README.md diff --git a/e2e/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx rename to e2e-playwright/test-plugins/grafana-test-datasource/components/ConfigEditor.tsx diff --git a/e2e/test-plugins/grafana-test-datasource/components/QueryEditor.tsx b/e2e-playwright/test-plugins/grafana-test-datasource/components/QueryEditor.tsx similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/components/QueryEditor.tsx rename to e2e-playwright/test-plugins/grafana-test-datasource/components/QueryEditor.tsx diff --git a/e2e/test-plugins/grafana-test-datasource/datasource.ts b/e2e-playwright/test-plugins/grafana-test-datasource/datasource.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/datasource.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/datasource.ts diff --git a/e2e/test-plugins/grafana-test-datasource/img/logo.svg b/e2e-playwright/test-plugins/grafana-test-datasource/img/logo.svg similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/img/logo.svg rename to e2e-playwright/test-plugins/grafana-test-datasource/img/logo.svg diff --git a/e2e/test-plugins/grafana-test-datasource/module.ts b/e2e-playwright/test-plugins/grafana-test-datasource/module.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/module.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/module.ts diff --git a/e2e/test-plugins/grafana-test-datasource/package.json b/e2e-playwright/test-plugins/grafana-test-datasource/package.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/package.json rename to e2e-playwright/test-plugins/grafana-test-datasource/package.json diff --git a/e2e/test-plugins/grafana-test-datasource/plugin.json b/e2e-playwright/test-plugins/grafana-test-datasource/plugin.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/plugin.json rename to e2e-playwright/test-plugins/grafana-test-datasource/plugin.json diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts b/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts new file mode 100644 index 00000000000..ed38635cd86 --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts @@ -0,0 +1,47 @@ +import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; + +// The following tests verify that label and input field association is working correctly. +// If these tests break, e2e tests in external plugins will break too. + +test.describe( + 'grafana-test-datasource', + { + tag: ['@plugins'], + }, + () => { + test.describe('config editor ', () => { + let configPage: DataSourceConfigPage; + test.beforeEach(async ({ createDataSourceConfigPage }) => { + configPage = await createDataSourceConfigPage({ type: 'grafana-e2etest-datasource' }); + }); + + test('text input field', async ({ page }) => { + const field = page.getByRole('textbox', { name: 'API key' }); + await expect(field).toBeEmpty(); + await field.fill('test text'); + await expect(field).toHaveValue('test text'); + }); + + test('switch field', async ({ page }) => { + const field = page.getByLabel('Switch Enabled'); + await expect(field).not.toBeChecked(); + await field.check(); + await expect(field).toBeChecked(); + }); + + test('checkbox field', async ({ page }) => { + const field = page.getByRole('checkbox', { name: 'Checkbox Enabled' }); + await expect(field).not.toBeChecked(); + await field.check({ force: true }); + await expect(field).toBeChecked(); + }); + + test('select field', async ({ page, selectors }) => { + const field = page.getByRole('combobox', { name: 'Auth type' }); + await field.click(); + const option = selectors.components.Select.option; + await expect(configPage.getByGrafanaSelector(option)).toHaveText(['keys', 'credentials']); + }); + }); + } +); diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts b/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts new file mode 100644 index 00000000000..5c1e636da7e --- /dev/null +++ b/e2e-playwright/test-plugins/grafana-test-datasource/tests/variables.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'grafana-test-datasource', + { + tag: ['@plugins'], + }, + () => { + test('should render variable editor', async ({ variableEditPage, page }) => { + await variableEditPage.datasource.set('gdev-e2etestdatasource'); + await expect(page.getByRole('textbox', { name: 'Query Text' })).toBeVisible(); + }); + + test('create new, successful variable query', async ({ variableEditPage, page }) => { + await variableEditPage.datasource.set('gdev-e2etestdatasource'); + await page.getByRole('textbox', { name: 'Query Text' }).fill('variableQuery'); + await variableEditPage.runQuery(); + await expect(variableEditPage).toDisplayPreviews(['A', 'B']); + }); + } +); diff --git a/e2e/test-plugins/grafana-test-datasource/tsconfig.json b/e2e-playwright/test-plugins/grafana-test-datasource/tsconfig.json similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/tsconfig.json rename to e2e-playwright/test-plugins/grafana-test-datasource/tsconfig.json diff --git a/e2e/test-plugins/grafana-test-datasource/types.ts b/e2e-playwright/test-plugins/grafana-test-datasource/types.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/types.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/types.ts diff --git a/e2e/test-plugins/grafana-test-datasource/variables.ts b/e2e-playwright/test-plugins/grafana-test-datasource/variables.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/variables.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/variables.ts diff --git a/e2e/test-plugins/grafana-test-datasource/webpack.config.ts b/e2e-playwright/test-plugins/grafana-test-datasource/webpack.config.ts similarity index 100% rename from e2e/test-plugins/grafana-test-datasource/webpack.config.ts rename to e2e-playwright/test-plugins/grafana-test-datasource/webpack.config.ts diff --git a/e2e-playwright/utils/dashboard-helpers.ts b/e2e-playwright/utils/dashboard-helpers.ts new file mode 100644 index 00000000000..ff73bfe919f --- /dev/null +++ b/e2e-playwright/utils/dashboard-helpers.ts @@ -0,0 +1,35 @@ +import { Page, expect } from '@playwright/test'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * Add a new dashboard + */ +export async function addDashboard(page: Page, title?: string): Promise { + const dashboardTitle = title || `e2e-${uuidv4()}`; + + // Navigate to add dashboard page + await page.goto('/dashboard/new'); + + // Save the dashboard + const saveButton = page.getByTestId('data-testid Save dashboard button'); + await saveButton.click(); + + // Enter dashboard title + const titleInput = page.getByLabel('Save dashboard title field'); + await titleInput.clear(); + await titleInput.fill(dashboardTitle); + + // Click save + const saveAsButton = page.getByTestId('data-testid Save dashboard drawer button'); + await saveAsButton.click(); + + // Wait for success notification + await expect(page.getByText('Dashboard saved')).toBeVisible(); + + // Get the dashboard UID from the URL + const url = page.url(); + const uidMatch = url.match(/\/d\/([^\/\?]+)/); + const uid = uidMatch ? uidMatch[1] : ''; + + return uid; +} diff --git a/e2e-playwright/utils/prometheus-helpers.ts b/e2e-playwright/utils/prometheus-helpers.ts new file mode 100644 index 00000000000..229fee2bf33 --- /dev/null +++ b/e2e-playwright/utils/prometheus-helpers.ts @@ -0,0 +1,52 @@ +import { Page } from '@playwright/test'; + +export async function getResources(page: Page): Promise { + // Mock the Prometheus API responses + await page.route(/__name__/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: ['metric1', 'metric2'], + }), + }); + }); + + await page.route(/metadata/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: { + metric1: [ + { + type: 'counter', + help: 'metric1 help', + unit: '', + }, + ], + metric2: [ + { + type: 'counter', + help: 'metric2 help', + unit: '', + }, + ], + }, + }), + }); + }); + + await page.route(/labels/g, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + status: 'success', + data: ['__name__', 'action', 'active', 'backend'], + }), + }); + }); +} diff --git a/e2e-playwright/various-suite/bar-gauge.spec.ts b/e2e-playwright/various-suite/bar-gauge.spec.ts new file mode 100644 index 00000000000..ecd07b7eb9b --- /dev/null +++ b/e2e-playwright/various-suite/bar-gauge.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test( + 'Various Suite test: Bar Gauge Panel', + { + tag: ['@various'], + }, + async ({ gotoDashboardPage, selectors, page }) => { + await gotoDashboardPage({ uid: 'O6f11TZWk' }); + + const barGaugeElement = page + .locator('[data-viz-panel-key="panel-6"]') + .getByTestId(selectors.components.Panels.Visualization.BarGauge.valueV2) + .first(); + + await expect(barGaugeElement).toBeVisible(); + await expect(barGaugeElement).toHaveCSS('color', 'rgb(242, 73, 92)'); + await expect(barGaugeElement).toContainText('100'); + } +); diff --git a/e2e-playwright/various-suite/bookmarks.spec.ts b/e2e-playwright/various-suite/bookmarks.spec.ts new file mode 100644 index 00000000000..9c67884ea7e --- /dev/null +++ b/e2e-playwright/various-suite/bookmarks.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Pin nav items', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + // Set localStorage to dock the navigation + await page.evaluate(() => { + localStorage.setItem('grafana.navigation.docked', 'true'); + }); + }); + + test('should pin the selected menu item and add it as a Bookmarks menu item child', async ({ page, selectors }) => { + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + + const navList = navMenu.locator('ul[aria-label="Navigation"]'); + await expect(navList).toBeVisible(); + + // Check if the Bookmark section is visible + const bookmarksItem = navList.locator('li').nth(1); + await expect(bookmarksItem).toBeVisible(); + await expect(bookmarksItem).toContainText('Bookmarks'); + + // Check if the Administration section is visible + const adminItem = navList.locator('li').last(); + await expect(adminItem).toBeVisible(); + await expect(adminItem).toContainText('Administration'); + + // Click the "Add to Bookmarks" button in the Administration section + const addToBookmarksButton = adminItem.getByLabel('Add to Bookmarks'); + await addToBookmarksButton.click({ force: true }); + + // Check if the Administration menu item is visible in the Bookmarks section + const expandBookmarksButton = bookmarksItem.locator('button[aria-label="Expand section: Bookmarks"]'); + await expect(expandBookmarksButton).toBeVisible(); + await expandBookmarksButton.click({ force: true }); + + const administrationLink = bookmarksItem.locator('a').filter({ hasText: 'Administration' }); + await expect(administrationLink).toBeVisible(); + }); + + test('should unpin the item and remove it from the Bookmarks section', async ({ page, selectors }) => { + // Set Administration as a pinned item + await page.evaluate(() => { + localStorage.setItem( + 'grafana.user.preferences', + JSON.stringify({ + navbar: { bookmarkUrls: ['/admin'] }, + }) + ); + }); + + // Visit the home page + await page.goto('/'); + + // Reload the page to apply the docked navigation + await page.reload(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + + const navList = navMenu.locator('ul[aria-label="Navigation"]'); + await expect(navList).toBeVisible(); + + // Check if the Bookmark section is visible + const bookmarksItem = navList.locator('li').nth(1); + await expect(bookmarksItem).toBeVisible(); + await expect(bookmarksItem).toContainText('Bookmarks'); + + // Expand the Bookmarks section + const expandBookmarksButton = bookmarksItem.getByLabel('Expand section: Bookmarks'); + await expandBookmarksButton.click({ force: true }); + + // Check that Administration is visible in bookmarks + const administrationLink = bookmarksItem.locator('a').filter({ hasText: 'Administration' }); + await expect(administrationLink).toBeVisible(); + + // Click the "Remove from Bookmarks" button + const removeFromBookmarksButton = bookmarksItem.getByLabel('Remove from Bookmarks'); + await removeFromBookmarksButton.click({ force: true }); + + // Check that Administration is no longer in bookmarks + await expect(bookmarksItem.locator('a')).toHaveCount(1); + await expect(bookmarksItem.locator('a').filter({ hasText: 'Administration' })).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/exemplars.spec.ts b/e2e-playwright/various-suite/exemplars.spec.ts new file mode 100644 index 00000000000..1e2f77aa04d --- /dev/null +++ b/e2e-playwright/various-suite/exemplars.spec.ts @@ -0,0 +1,150 @@ +import { Page } from '@playwright/test'; + +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Exemplars', + { + tag: ['@various'], + }, + () => { + const dataSourceName = 'PromExemplar'; + + async function addDataSource(page, selectors, createDataSourceConfigPage) { + // Navigate to add data source page + await createDataSourceConfigPage({ type: 'prometheus', name: dataSourceName }); + + // Add exemplars configuration + const exemplarsAddButton = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.exemplarsAddButton + ); + await exemplarsAddButton.click(); + + const internalLinkSwitch = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.internalLinkSwitch + ); + await internalLinkSwitch.check({ force: true }); + + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.click(); + await page.keyboard.type('http://prom-url:9090'); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await dataSourcePicker.click(); + + const tempoOption = page.getByText('gdev-tempo'); + await tempoOption.scrollIntoViewIfNeeded(); + await expect(tempoOption).toBeVisible(); + await tempoOption.click(); + + // Save the data source + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + } + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + await addDataSource(page, selectors, createDataSourceConfigPage); + await page.goto('/'); + }); + + test('should be able to navigate to configured data source', async ({ page, selectors }) => { + // Mock API responses + await page.route(/api\/ds\/query/, async (route) => { + const postData = route.request().postDataJSON(); + const datasourceType = postData.queries[0].datasource.type; + + if (datasourceType === 'prometheus') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(require('../fixtures/exemplars-query-response.json')), + }); + } else if (datasourceType === 'tempo') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(require('../fixtures/tempo-response.json')), + }); + } else { + await route.fulfill({ status: 200, body: '{}' }); + } + }); + + // Navigate to explore + await page.goto('/explore'); + + // Select data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(dataSourceName); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + + // Switch to code editor + const codeRadioButton = page.getByTestId(selectors.components.RadioButton.container).filter({ hasText: 'Code' }); + await codeRadioButton.click(); + + // Wait for lazy loading Monaco + await waitForMonacoToLoad(page); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.clear(); + await fromField.fill('2021-07-10 17:10:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.clear(); + await toField.fill('2021-07-10 17:30:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + // Type query + const queryField = page.getByTestId(selectors.components.QueryField.container); + await expect(queryField).toBeVisible(); + await queryField.click(); + await page.keyboard.type('exemplar-query_bucket'); + await page.keyboard.press('Shift+Enter'); + + await page.waitForTimeout(1000); + + // Click zoom to data + const zoomToDataButton = page.getByTestId('time-series-zoom-to-data'); + await zoomToDataButton.click(); + + // Hover over exemplar marker and click + const exemplarMarker = page.getByTestId(selectors.components.DataSource.Prometheus.exemplarMarker).first(); + await exemplarMarker.hover(); + + const queryWithTempoLink = page.getByText('Query with gdev-tempo'); + await queryWithTempoLink.click(); + + // Verify trace viewer has span bars + const spanBars = page.getByTestId(selectors.components.TraceViewer.spanBar); + await expect(spanBars).toHaveCount(11); + }); + } +); + +export async function waitForMonacoToLoad(page: Page) { + // Wait for spinner to disappear + const spinner = page.getByTestId('Spinner'); + await expect(spinner).toBeHidden(); + + // Wait for Monaco to be available in window + await page.waitForFunction(() => { + return window.monaco !== undefined; + }); + + // Wait for Monaco editor textarea to exist + const monacoTextarea = page.locator('.monaco-editor').first(); + await expect(monacoTextarea).toBeVisible(); +} diff --git a/e2e-playwright/various-suite/explore.spec.ts b/e2e-playwright/various-suite/explore.spec.ts new file mode 100644 index 00000000000..afbc9d74c32 --- /dev/null +++ b/e2e-playwright/various-suite/explore.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Explore', + { + tag: ['@various'], + }, + () => { + test('Basic path through Explore.', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const exploreContainer = dashboardPage.getByGrafanaSelector(selectors.pages.Explore.General.container); + await expect(exploreContainer).toHaveCount(1); + + const refreshButton = dashboardPage.getByGrafanaSelector(selectors.components.RefreshPicker.runButtonV2); + await expect(refreshButton).toHaveCount(1); + + const scenarioSelectContainer = dashboardPage.getByGrafanaSelector( + selectors.components.DataSource.TestData.QueryTab.scenarioSelectContainer + ); + await expect(scenarioSelectContainer).toBeVisible(); + await scenarioSelectContainer.locator('input[id*="test-data-scenario-select-"]').click(); + + const csvMetricValues = page.getByText('CSV Metric Values'); + await expect(csvMetricValues).toBeVisible(); + await csvMetricValues.click(); + }); + } +); diff --git a/e2e-playwright/various-suite/filter-annotations.spec.ts b/e2e-playwright/various-suite/filter-annotations.spec.ts new file mode 100644 index 00000000000..7e51d607864 --- /dev/null +++ b/e2e-playwright/various-suite/filter-annotations.spec.ts @@ -0,0 +1,121 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Annotations filtering', + { + tag: ['@various'], + }, + () => { + const DASHBOARD_ID = 'ed155665'; + + test('Tests switching filter type updates the UI accordingly', async ({ page, selectors, gotoDashboardPage }) => { + // Navigate to dashboard + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID }); + + // Enter edit mode + const editButton = dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + // Open settings + const settingsButton = dashboardPage.getByGrafanaSelector( + selectors.components.NavToolbar.editDashboard.settingsButton + ); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + // Navigate to annotations tab + const annotationsTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Annotations')); + await annotationsTab.click(); + + // Click "New query" + const newQueryButton = page.getByText('New query'); + await newQueryButton.click(); + + // Set annotation name + const nameInput = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Annotations.Settings.name + ); + await nameInput.fill('Red - Panel two'); + + // Test filter type switching + const showInLabel = dashboardPage.getByGrafanaSelector( + selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel + ); + await expect(showInLabel).toBeVisible(); + + // Test "All panels" option + const annotationsTypeInput = dashboardPage + .getByGrafanaSelector(selectors.components.Annotations.annotationsTypeInput) + .locator('input'); + await annotationsTypeInput.fill('All panels'); + await annotationsTypeInput.press('Enter'); + + const annotationsChoosePanelInput = dashboardPage.getByGrafanaSelector( + selectors.components.Annotations.annotationsChoosePanelInput + ); + await expect(annotationsChoosePanelInput).toBeHidden(); + + // Test "All panels except" option + await annotationsTypeInput.fill('All panels except'); + await annotationsTypeInput.press('Enter'); + await expect(annotationsChoosePanelInput).toBeVisible(); + + // Test "Selected panels" option + await annotationsTypeInput.fill('Selected panels'); + await annotationsTypeInput.press('Enter'); + await expect(annotationsChoosePanelInput).toBeVisible(); + + // Select "Panel two" + await annotationsTypeInput.fill('Panel two'); + await annotationsTypeInput.press('Enter'); + + // Click outside to close dropdown + await page.click('body', { position: { x: 0, y: 0 } }); + + // Go back to dashboard + const backToDashboardButton = dashboardPage.getByGrafanaSelector( + selectors.components.NavToolbar.editDashboard.backToDashboardButton + ); + await expect(backToDashboardButton).toBeVisible(); + await backToDashboardButton.click(); + + // Test annotation controls + const dashboardControls = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Controls); + await expect(dashboardControls).toBeVisible(); + + // Test "Red - Panel two" annotation toggle + const redPanelTwoLabel = page.getByLabel('Red - Panel two'); + await expect(redPanelTwoLabel).toBeVisible(); + + const redPanelTwoToggleParent = page.getByLabel('Red - Panel two').locator('..'); + const redPanelTwoCheckbox = redPanelTwoToggleParent.locator('input'); + + // Check initial state + await expect(redPanelTwoCheckbox).toBeChecked(); + + // Uncheck and verify + await redPanelTwoCheckbox.uncheck({ force: true }); + await expect(redPanelTwoCheckbox).toBeChecked({ checked: false }); + + // Check again and verify + await redPanelTwoCheckbox.check({ force: true }); + await expect(redPanelTwoCheckbox).toBeChecked(); + + // Test "Red, only panel 1" annotation toggle + const redOnlyPanelOneLabel = page.getByLabel('Red, only panel 1'); + await expect(redOnlyPanelOneLabel).toBeVisible(); + + const redOnlyPanelOneToggleParent = page.getByLabel('Red, only panel 1').locator('..'); + const redOnlyPanelOneCheckbox = redOnlyPanelOneToggleParent.locator('input'); + await expect(redOnlyPanelOneCheckbox).toBeChecked(); + + // Verify annotations in panel + const panelOne = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel one')); + await expect(panelOne).toBeVisible(); + + const annotationMarkers = panelOne.getByTestId(selectors.pages.Dashboard.Annotations.marker); + await expect(annotationMarkers).toHaveCount(4); + }); + } +); diff --git a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts new file mode 100644 index 00000000000..dfa19753b15 --- /dev/null +++ b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const APP_ID = 'sandbox-app-test'; + +test.describe( + 'Datasource sandbox', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + await page.request.post(`/api/plugins/${APP_ID}/settings`, { + data: { + enabled: true, + }, + }); + }); + + test.describe('App Page', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Loads the app page without the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeHidden(); + + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); + }); + + test('Loads the app configuration without the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeHidden(); + + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Loads the app page with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/a/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); + + const appPage = page.getByTestId('sandbox-app-test-page-one'); + await expect(appPage).toBeVisible(); + }); + + test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { + await page.goto(`/plugins/${APP_ID}`); + + const sandboxDiv = page.locator('div[data-plugin-sandbox="sandbox-app-test"]'); + await expect(sandboxDiv).toBeVisible(); + + const configPage = page.getByTestId('sandbox-app-test-config-page'); + await expect(configPage).toBeVisible(); + }); + }); + }); + } +); diff --git a/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts new file mode 100644 index 00000000000..df0f32aeba0 --- /dev/null +++ b/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @@ -0,0 +1,203 @@ +import { random } from 'lodash'; + +import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; + +const DATASOURCE_ID = 'sandbox-test-datasource'; +let DATASOURCE_CONNECTION_ID = ''; +const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; + +// Originally skipped due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts +// TODO: fix and remove skip +test.describe.skip( + 'Datasource sandbox', + { + tag: ['@various', '@wip'], + }, + () => { + let configPage: DataSourceConfigPage; + test.beforeEach(async ({ createDataSourceConfigPage }) => { + // Add the datasource + configPage = await createDataSourceConfigPage({ + type: 'sandbox', + name: DATASOURCE_TYPED_NAME, + }); + }); + + test.describe('Config Editor', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Should not render a sandbox wrapper around the datasource config editor', async ({ page }) => { + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await page.waitForTimeout(300); // wait to prevent false positives because playwright checks too fast + + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeHidden(); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Should render a sandbox wrapper around the datasource config editor', async ({ page, selectors }) => { + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); + }); + + test('Should store values in jsonData and secureJsonData correctly', async ({ page }) => { + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + + const valueToStore = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-config-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToStore); + await expect(queryInput).toHaveValue(valueToStore); + + const saveButton = page.getByTestId('data-testid Data source settings page Save and test button'); + await saveButton.click(); + + const alert = page.locator('[data-testid="data-testid Alert"]'); + await expect(alert).toBeVisible(); + await expect(alert).toContainText('Sandbox Success'); + + // validate the value was stored + await page.goto(`/connections/datasources/edit/${DATASOURCE_CONNECTION_ID}`); + await expect(queryInput).not.toBeDisabled(); + await expect(queryInput).toHaveValue(valueToStore); + }); + }); + }); + + test.describe('Explore Page', () => { + test.describe('Sandbox disabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0'); + }); + }); + + test('Should not wrap the query editor in a sandbox wrapper', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + await page.waitForTimeout(300); // wait to prevent false positives because playwright checks too fast + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeHidden(); + }); + + test('Should accept values when typed', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const valueToType = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); + }); + }); + + test.describe('Sandbox enabled', () => { + test.beforeEach(async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1'); + }); + }); + + test('Should wrap the query editor in a sandbox wrapper', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const sandboxDiv = page.locator(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`); + await expect(sandboxDiv).toBeVisible(); + }); + + test('Should accept values when typed', async ({ page, dashboardPage, selectors }) => { + await page.goto('/explore'); + + const dataSourcePicker = dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const datasourceOption = page.locator(`text=${DATASOURCE_TYPED_NAME}`); + await expect(datasourceOption).toBeVisible(); + await datasourceOption.scrollIntoViewIfNeeded(); + await datasourceOption.click(); + + // make sure the datasource was correctly selected and rendered + const breadcrumb = dashboardPage.getByGrafanaSelector( + selectors.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME) + ); + await expect(breadcrumb).toBeVisible(); + + const valueToType = 'test' + random(100); + + const queryInput = page.locator('[data-testid="sandbox-query-editor-query-input"]'); + await expect(queryInput).not.toBeDisabled(); + await queryInput.fill(valueToType); + await expect(queryInput).toHaveValue(valueToType); + + // typing the query editor should reflect in the url + await expect(page).toHaveURL(new RegExp(valueToType)); + }); + }); + }); + + test.afterEach(async ({ page }) => { + // Clear cookies after each test + await page.context().clearCookies(); + }); + } +); diff --git a/e2e-playwright/various-suite/gauge.spec.ts b/e2e-playwright/various-suite/gauge.spec.ts new file mode 100644 index 00000000000..3e2d6023c95 --- /dev/null +++ b/e2e-playwright/various-suite/gauge.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +// this test requires a larger viewport so all gauge panels load properly +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +test.describe( + 'Gauge Panel', + { + tag: ['@various'], + }, + () => { + test('Gauge rendering e2e tests', async ({ gotoDashboardPage, selectors, page }) => { + // open Panel Tests - Gauge + const dashboardPage = await gotoDashboardPage({ uid: '_5rDmaQiz' }); + + // check that gauges are rendered + const gaugeElements = page.locator('.flot-base'); + await expect(gaugeElements).toHaveCount(16); + + // check that no panel errors exist + const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error')); + await expect(errorInfo).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/graph-auto-migrate.spec.ts b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts new file mode 100644 index 00000000000..8b1234a8160 --- /dev/null +++ b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts @@ -0,0 +1,71 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const DASHBOARD_ID = 'XMjIZPmik'; +const DASHBOARD_NAME = 'Panel Tests - Graph Time Regions'; +const UPLOT_MAIN_DIV_SELECTOR = 'uplot-main-div'; +const ANNOTATION_MARKER_SELECTOR = 'data-testid annotation-marker'; + +test.describe( + 'Auto-migrate graph panel', + { + tag: ['@various'], + }, + () => { + test('Graph panel is auto-migrated', async ({ gotoDashboardPage, page }) => { + await gotoDashboardPage({ uid: DASHBOARD_ID }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden(); + + await gotoDashboardPage({ uid: DASHBOARD_ID }); + + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeVisible(); + }); + + test('Annotation markers exist for time regions', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID }); + await expect(page.getByText(DASHBOARD_NAME)).toBeVisible(); + await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden(); + + await gotoDashboardPage({ uid: DASHBOARD_ID }); + + // Check Business Hours panel + const businessHoursPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Business Hours') + ); + await expect(businessHoursPanel).toBeVisible(); + const businessHoursMarker = businessHoursPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(businessHoursMarker).toBeVisible(); + + // Check Sunday's 20-23 panel + const sundayPanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title("Sunday's 20-23")); + await expect(sundayPanel).toBeVisible(); + const sundayMarker = sundayPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(sundayMarker).toBeVisible(); + + // Check Each day of week panel + const eachDayPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('Each day of week') + ); + await expect(eachDayPanel).toBeVisible(); + const eachDayMarker = eachDayPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(eachDayMarker).toBeVisible(); + + // Scroll to bottom + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + // Check 05:00 panel + const timePanel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('05:00')); + await expect(timePanel).toBeVisible(); + const timeMarker = timePanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(timeMarker).toBeVisible(); + + // Check From 22:00 to 00:30 panel + const midnightPanel = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title('From 22:00 to 00:30 (crossing midnight)') + ); + await expect(midnightPanel).toBeVisible(); + const midnightMarker = midnightPanel.getByTestId(ANNOTATION_MARKER_SELECTOR).first(); + await expect(midnightMarker).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/inspect-drawer.spec.ts b/e2e-playwright/various-suite/inspect-drawer.spec.ts new file mode 100644 index 00000000000..60f0dd42b06 --- /dev/null +++ b/e2e-playwright/various-suite/inspect-drawer.spec.ts @@ -0,0 +1,173 @@ +import { Page } from 'playwright-core'; + +import { test, expect, DashboardPage, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +const PANEL_UNDER_TEST = 'Value reducers 1'; + +test.describe( + 'Inspect drawer tests', + { + tag: ['@various'], + }, + () => { + test('Tests various Inspect Drawer scenarios', async ({ gotoDashboardPage, selectors, page }) => { + const dashboardPage = await gotoDashboardPage({ uid: 'wfTJJL5Wz' }); + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await panel.scrollIntoViewIfNeeded(); + await expect(panel).toBeVisible(); + + // Open panel menu + const panelMenu = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)); + await panelMenu.click({ force: true }); + + // Hover over Inspect menu item to show submenu + const inspectMenuItem = dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.menuItems('Inspect') + ); + await inspectMenuItem.hover(); + + // Click on Data submenu item + const dataMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Data')); + await dataMenuItem.click(); + + await expectDrawerTabsAndContent(dashboardPage, selectors, page); + + await expectDrawerClose(dashboardPage, selectors); + + await expectSubMenuScenario(dashboardPage, selectors, page, 'Data'); + await expectSubMenuScenario(dashboardPage, selectors, page, 'Query'); + await expectSubMenuScenario(dashboardPage, selectors, page, 'Panel JSON', 'JSON'); + + // Test edit panel scenario + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)) + .click({ force: true }); + const editMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Edit')); + await editMenuItem.click(); + + const queryInspectorButton = dashboardPage.getByGrafanaSelector( + selectors.components.QueryTab.queryInspectorButton + ); + await expect(queryInspectorButton).toBeVisible(); + await queryInspectorButton.click(); + + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeVisible(); + + const queryTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Query')); + await expect(queryTab).toBeVisible(); + + // Query should be the active tab + const activeTab = page.locator('a[class*="-activeTabStyle"]'); + await expect(activeTab).toHaveText('Query'); + + const queryContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content); + await expect(queryContent).toBeVisible(); + }); + } +); + +const expectDrawerTabsAndContent = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups, page: Page) => { + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeVisible(); + + const dataTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Data')); + await expect(dataTab).toBeVisible(); + + // Data should be the active tab + const activeTab = page.locator(selectors.components.Tab.active('')); + await expect(activeTab).toHaveText('Data'); + + const dataContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content); + await expect(dataContent).toBeVisible(); + + const statsContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content); + await expect(statsContent).toBeHidden(); + + const jsonContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content); + await expect(jsonContent).toBeHidden(); + + const queryContent = dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content); + await expect(queryContent).toBeHidden(); + + // Test Stats tab + const statsTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Stats')); + await expect(statsTab).toBeVisible(); + await statsTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeHidden(); + + // Test JSON tab + const jsonTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('JSON')); + await expect(jsonTab).toBeVisible(); + await jsonTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeHidden(); + + // Test Query tab + const queryTab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Query')); + await expect(queryTab).toBeVisible(); + await queryTab.click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Query.content)).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Data.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Stats.content)).toBeHidden(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeHidden(); +}; + +const expectDrawerClose = async (dashboardPage: DashboardPage, selectors: E2ESelectorGroups) => { + // Close using close button + const closeButton = dashboardPage.getByGrafanaSelector(selectors.components.Drawer.General.close); + await closeButton.click(); + + const drawerTitle = dashboardPage.getByGrafanaSelector( + selectors.components.Drawer.General.title(`Inspect: ${PANEL_UNDER_TEST}`) + ); + await expect(drawerTitle).toBeHidden(); +}; + +const expectSubMenuScenario = async ( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + page: Page, + subMenu: string, + tabTitle?: string +) => { + tabTitle = tabTitle ?? subMenu; + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_UNDER_TEST)); + await panel.scrollIntoViewIfNeeded(); + await expect(panel).toBeVisible(); + + // Open panel menu + const panelMenu = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menu(PANEL_UNDER_TEST)); + await panelMenu.click({ force: true }); + + // Hover over Inspect menu item to show submenu + const inspectMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems('Inspect')); + await inspectMenuItem.hover(); + + // Click on submenu item + const subMenuItem = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.menuItems(subMenu)); + await subMenuItem.click(); + + // Tab should be visible and active + const tab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabTitle)); + await expect(tab).toBeVisible(); + + const activeTab = page.locator(selectors.components.Tab.active('')); + await expect(activeTab).toHaveText(tabTitle); + + await expectDrawerClose(dashboardPage, selectors); +}; diff --git a/e2e-playwright/various-suite/keybinds.spec.ts b/e2e-playwright/various-suite/keybinds.spec.ts new file mode 100644 index 00000000000..4ff37889838 --- /dev/null +++ b/e2e-playwright/various-suite/keybinds.spec.ts @@ -0,0 +1,89 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Keyboard shortcuts', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page, selectors }) => { + await page.goto('/'); + + // Wait for the page to load + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Latest from the blog')); + await expect(panelTitle).toBeVisible(); + }); + + test('sequence shortcuts should work', async ({ page, selectors }) => { + // Navigate to explore with 'ge' shortcut + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Navigate to profile with 'gp' shortcut + await page.keyboard.type('gp'); + const preferencesSaveButton = page.getByTestId(selectors.components.UserProfile.preferencesSaveButton); + await expect(preferencesSaveButton).toBeVisible(); + + // Navigate back to home with 'gh' shortcut + await page.keyboard.type('gh'); + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Latest from the blog')); + await expect(panelTitle).toBeVisible(); + }); + + test('ctrl+z should zoom out the time range', async ({ page, selectors }) => { + // Navigate to explore + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.fill('2024-06-05 10:05:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.fill('2024-06-05 10:06:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + await page.keyboard.press('Control+z'); + + const expectedRange = 'Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30'; + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + }); + + test('time range shortcuts should work', async ({ page, selectors }) => { + // Navigate to explore + await page.keyboard.type('ge'); + const exploreContainer = page.getByTestId(selectors.pages.Explore.General.container); + await expect(exploreContainer).toBeVisible(); + + // Set time range + const timePickerButton = page.getByTestId(selectors.components.TimePicker.openButton); + await timePickerButton.click(); + + const fromField = page.getByTestId(selectors.components.TimePicker.fromField); + await fromField.fill('2024-06-05 10:05:00'); + + const toField = page.getByTestId(selectors.components.TimePicker.toField); + await toField.fill('2024-06-05 10:06:00'); + + const applyTimeRangeButton = page.getByTestId(selectors.components.TimePicker.applyTimeRange); + await applyTimeRangeButton.click(); + + let expectedRange = 'Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00'; + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + + // Use time range shortcut to move back + await page.keyboard.press('t'); + await page.keyboard.press('ArrowLeft'); + + expectedRange = 'Time range selected: 2024-06-05 10:04:00 to 2024-06-05 10:05:00'; // 1 min back + await expect(timePickerButton).toHaveAttribute('aria-label', expectedRange); + }); + } +); diff --git a/e2e-playwright/various-suite/loki-query-builder.spec.ts b/e2e-playwright/various-suite/loki-query-builder.spec.ts new file mode 100644 index 00000000000..47e3c35299f --- /dev/null +++ b/e2e-playwright/various-suite/loki-query-builder.spec.ts @@ -0,0 +1,112 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const MISSING_LABEL_FILTER_ERROR_MESSAGE = 'Select at least 1 label filter (label and value)'; +const dataSourceName = 'LokiBuilder'; +const finalQuery = 'rate({instance=~"instance1|instance2"} | logfmt | __error__=`` [$__auto]'; + +test.describe( + 'Loki query builder', + { + tag: ['@various'], + }, + () => { + test('should be able to use all modes', async ({ createDataSource, page, dashboardPage, selectors }) => { + await createDataSource({ type: 'loki', name: dataSourceName }); + // Mock API responses + await page.route(/labels\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance', 'job', 'source'] }), + }); + }); + + await page.route(/series?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: [{ instance: 'instance1' }] }), + }); + }); + + await page.route(/values/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance1', 'instance2'] }), + }); + }); + + await page.route(/index\/stats/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ streams: 2, chunks: 2660, bytes: 2721792, entries: 14408 }), + }); + }); + + // Go to Explore and choose Loki data source + await page.goto('/explore'); + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + await page.getByRole('button', { name: dataSourceName }).click(); + + // Start in builder mode, click and choose query pattern + await page.getByTestId(selectors.components.QueryBuilder.queryPatterns).click(); + await page.getByRole('button', { name: 'Log query starters' }).click(); + await page.getByRole('button', { name: 'Use this query' }).first().click(); + await expect(page.getByText('No pipeline errors')).toBeVisible(); + await expect(page.getByText('{} | logfmt | __error__=``')).toBeVisible(); + + // Add operation + await page.getByRole('button', { name: 'Operations', exact: true }).click(); + await page.getByText('Range functions').click(); + await page.getByText('Rate', { exact: true }).click(); + await expect(page.getByText('rate({} | logfmt | __error__=`` [$__auto]')).toBeVisible(); + + // Check for expected error + await expect(page.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeVisible(); + + // Add labels to remove error + await dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.labelSelect).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.inputSelect).fill('instance'); + await page.keyboard.press('Enter'); + + const matchOperatorSelect = dashboardPage.getByGrafanaSelector( + selectors.components.QueryBuilder.matchOperatorSelect + ); + await expect(matchOperatorSelect).toBeVisible(); + await matchOperatorSelect.click({ force: true }); + + const matchOperatorInput = matchOperatorSelect.locator('div').locator('input'); + await matchOperatorInput.fill('=~'); + await page.keyboard.press('Enter'); + + const valueSelect = dashboardPage.getByGrafanaSelector(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + await valueSelect.click(); + + const valueInput = valueSelect.locator('div').locator('input'); + await valueInput.fill('instance1'); + await page.keyboard.press('Enter'); + await valueInput.fill('instance2'); + await page.keyboard.press('Enter'); + + await expect(page.getByText(MISSING_LABEL_FILTER_ERROR_MESSAGE)).toBeHidden(); + await expect(page.getByText(finalQuery)).toBeVisible(); + + // Change to code editor + await page.getByRole('radio', { name: 'Code' }).click(); + + // We need to test this manually because the final query is split into separate DOM elements + await expect(page.getByText('rate')).toBeVisible(); + await expect(page.getByText('instance1|instance2')).toBeVisible(); + await expect(page.getByText('logfmt')).toBeVisible(); + await expect(page.getByText('__error__')).toBeVisible(); + await expect(page.getByText('$__auto')).toBeVisible(); + + // Checks the explain mode toggle + await page.getByText('Explain').click(); + await expect(page.getByText('Fetch all log lines matching label filters.')).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts b/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts new file mode 100644 index 00000000000..8495d43c07c --- /dev/null +++ b/e2e-playwright/various-suite/loki-table-explore-to-dash.spec.ts @@ -0,0 +1,204 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const dataSourceName = 'LokiEditor'; + +const lokiQueryResult = { + status: 'success', + results: { + A: { + status: 200, + frames: [ + { + schema: { + refId: 'A', + meta: { + typeVersion: [0, 0], + custom: { + frameType: 'LabeledTimeValues', + }, + stats: [ + { + displayName: 'Summary: bytes processed per second', + unit: 'Bps', + value: 223921, + }, + { + displayName: 'Summary: total bytes processed', + unit: 'decbytes', + value: 4156, + }, + { + displayName: 'Summary: exec time', + unit: 's', + value: 0.01856, + }, + ], + executedQueryString: 'Expr: {targetLabelName="targetLabelValue"}', + }, + fields: [ + { + name: 'labels', + type: 'other', + typeInfo: { + frame: 'json.RawMessage', + }, + }, + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + }, + { + name: 'Line', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + { + name: 'tsNs', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + { + name: 'id', + type: 'string', + typeInfo: { + frame: 'string', + }, + }, + ], + }, + data: { + values: [ + [ + { + targetLabelName: 'targetLabelValue', + instance: 'server\\1', + job: '"grafana/data"', + nonIndexed: 'value', + place: 'moon', + re: 'one.two$three^four', + source: 'data', + }, + ], + [1700077283237], + [ + '{"_entry":"log text with ANSI \\u001b[31mpart of the text\\u001b[0m [149702545]","counter":"22292","float":"NaN","wave":-0.5877852522916832,"label":"val3","level":"info"}', + ], + ['1700077283237000000'], + ['1700077283237000000_9b025d35'], + ], + }, + }, + ], + }, + }, +}; + +test.use({ + featureToggles: { + logsExploreTableVisualisation: true, + }, +}); + +test.describe( + 'Loki Query Editor', + { + tag: ['@various'], + }, + () => { + test('Should be able to add explore table to dashboard', async ({ + createDataSource, + page, + dashboardPage, + selectors, + }) => { + await createDataSource({ type: 'loki', name: dataSourceName }); + // Mock API responses + await page.route(/labels\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: ['instance', 'job', 'source'] }), + }); + }); + + await page.route(/series\?/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ status: 'success', data: [{ instance: 'instance1' }] }), + }); + }); + + await page.route(/\/api\/ds\/query\?ds_type=loki/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(lokiQueryResult), + }); + }); + + // Go to Explore and choose Loki data source + await page.goto('/explore'); + await dashboardPage.getByGrafanaSelector(selectors.components.DataSourcePicker.container).click(); + await page.getByRole('button', { name: dataSourceName }).click(); + + await page.getByRole('radio', { name: 'Code' }).click(); + + // Write a simple query + const queryField = page.getByTestId(selectors.components.QueryField.container).locator('textarea'); + await queryField.fill('query{instance="instance1"}'); + + // Submit the query with Shift+Enter + await queryField.press('Shift+Enter'); + + // Assert the no-data message is not visible + await expect(page.locator('[data-testid="explore-no-data"]')).toBeHidden(); + + // Click on the table toggle + await page.getByRole('radio', { name: 'Table' }).click({ force: true }); + + // One row with two cells initially + const cells = page.locator('[role="cell"]'); + await expect(cells).toHaveCount(2); + + // Find and click on the targetLabelName label + await page.getByText('targetLabelName', { exact: true }).click(); + + // Now we should have a row with 3 columns + await expect(cells).toHaveCount(3); + // And a value of "targetLabelValue" + await expect(cells.getByText('targetLabelValue')).toBeVisible(); + + await page.getByLabel('Add', { exact: true }).click(); + + await page.getByLabel('Add to dashboard').click(); + + const addPanelToDashboardButton = page.getByText('Add panel to dashboard'); + await expect(addPanelToDashboardButton).toBeVisible(); + + await page.getByText('Open dashboard').click(); + + // Check the panel is visible + const panel = page.locator('[data-viz-panel-key="panel-1"]'); + await expect(panel).toBeVisible(); + + // Check the table cells in the panel + const panelCells = panel.locator('[role="table"] [role="cell"]'); + // Should have 3 columns + await expect(panelCells).toHaveCount(3); + + // Cells contain strings found in log line + await expect(page.getByText('"wave":-0.5877852522916832')).toBeVisible(); + + // Column has correct value of "targetLabelValue" + await expect(panel.locator('[role="table"] [role="cell"]').filter({ hasText: 'targetLabelValue' })).toBeVisible(); + }); + } +); diff --git a/e2e-playwright/various-suite/migrate-to-cloud.spec.ts b/e2e-playwright/various-suite/migrate-to-cloud.spec.ts new file mode 100644 index 00000000000..d2712554ea1 --- /dev/null +++ b/e2e-playwright/various-suite/migrate-to-cloud.spec.ts @@ -0,0 +1,393 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Migrate to Cloud (On-prem)', + { + tag: ['@various', '@wip'], + }, + () => { + test.describe('with mocked calls to the API backend', () => { + test.afterEach(async ({ page }) => { + const disconnectButton = page.getByTestId('migrate-to-cloud-summary-disconnect-button'); + await expect(disconnectButton).toBeVisible(); + await disconnectButton.click(); + }); + + const SESSION_UID = 'fehq6hqd246iox'; + const SNAPSHOT_UID1 = 'cehq6vdjqbqbkx'; + + const SNAPSHOT_RESULTS = [ + { name: 'FolderA', type: 'FOLDER', refId: 'ref-id-folder-a', parentName: 'General' }, + { name: 'FolderB', type: 'FOLDER', refId: 'ref-id-folder-b', parentName: 'General' }, + { name: 'Prometheus', type: 'DATASOURCE', refId: 'prometheus' }, + { name: 'Postgres', type: 'DATASOURCE', refId: 'postgres' }, + { name: 'Loki', type: 'DATASOURCE', refId: 'loki' }, + { name: 'Alert Rule A', type: 'ALERT_RULE', refId: 'alert-rule-a', parentName: 'FolderA' }, + { name: 'Alert Rule B', type: 'ALERT_RULE', refId: 'alert-rule-b', parentName: 'FolderB' }, + { name: 'Alert Rule C', type: 'ALERT_RULE', refId: 'alert-rule-c', parentName: 'FolderB' }, + { name: 'Alert Rule Group A', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-a', parentName: 'FolderA' }, + { name: 'Alert Rule Group B', type: 'ALERT_RULE_GROUP', refId: 'alert-rule-group-b', parentName: 'FolderB' }, + { name: 'Contact Point A', type: 'CONTACT_POINT', refId: 'contact-point-a' }, + { name: 'Contact Point B', type: 'CONTACT_POINT', refId: 'contact-point-b' }, + { name: 'Contact Point C', type: 'CONTACT_POINT', refId: 'contact-point-c' }, + { name: 'Notification Policy A', type: 'NOTIFICATION_POLICY', refId: 'notification-policy-a' }, + { name: 'Notification Template A', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-a' }, + { name: 'Notification Template B', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-b' }, + { name: 'Notification Template C', type: 'NOTIFICATION_TEMPLATE', refId: 'notification-template-c' }, + { name: 'Plugin A', type: 'PLUGIN', refId: 'plugin-a' }, + { name: 'Plugin B', type: 'PLUGIN', refId: 'plugin-b' }, + { name: 'Plugin C', type: 'PLUGIN', refId: 'plugin-c' }, + { name: 'Mute Timing A', type: 'MUTE_TIMING', refId: 'mute-timing-a' }, + { name: 'Mute Timing B', type: 'MUTE_TIMING', refId: 'mute-timing-b' }, + ]; + + const MIGRATION_SESSION = { + uid: SESSION_UID, + slug: 'test-slug', + created: '2025-04-02T21:36:08+02:00', + updated: '2025-04-02T21:36:08+02:00', + }; + + const STATS = { + types: SNAPSHOT_RESULTS.reduce>((acc, r) => { + acc[r.type] = (acc[r.type] || 0) + 1; + return acc; + }, {}), + statuses: { + PENDING: SNAPSHOT_RESULTS.length, + }, + total: SNAPSHOT_RESULTS.length, + }; + + // TODO: fix the test. It makes it most of the way through. Probably a network mock issue. + test.skip('creates and uploads a snapshot successfully', async ({ page }) => { + // Visit the migrate to cloud onprem page + await page.goto('/admin/migrate-to-cloud'); + + // Open the connect modal and enter the token + const connectButton = page.getByTestId('migrate-to-cloud-connect-session-modal-button'); + await expect(connectButton).toBeVisible(); + await connectButton.click(); + + const tokenInput = page.getByTestId('migrate-to-cloud-connect-session-modal-token-input'); + await expect(tokenInput).toBeVisible(); + await tokenInput.focus(); + await tokenInput.fill('test'); + + // Mock API responses + await page.route(/api\/cloudmigration\/migration/, async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(MIGRATION_SESSION), + }); + } else if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + sessions: [MIGRATION_SESSION], + }), + }); + } + }); + + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [], + }), + }); + }); + + // Click the connect button to create the token + const connectSessionButton = page.getByTestId('migrate-to-cloud-connect-session-modal-connect-button'); + await expect(connectSessionButton).toBeVisible(); + await connectSessionButton.click(); + + // Check the 'Include all' resources checkbox + const includeAllCheckbox = page.getByTestId( + 'migrate-to-cloud-configure-snapshot-checkbox-resource-include-all' + ); + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + // And validate that all resources are indeed checked + for (const resourceType of [ + 'alert_rule', + 'alert_rule_group', + 'contact_point', + 'dashboard', + 'datasource', + 'folder', + 'library_element', + 'mute_timing', + 'notification_policy', + 'notification_template', + 'plugin', + ]) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).toBeChecked(); + } + + // Remove one of the resources that has dependencies + // Mute Timings are dependencies of Alert Rules, which are dependencies of Alert Rule Groups + const muteTimingCheckbox = page.getByTestId( + 'migrate-to-cloud-configure-snapshot-checkbox-resource-mute_timing' + ); + await muteTimingCheckbox.uncheck({ force: true }); + await expect(muteTimingCheckbox).not.toBeChecked(); + + // Validate that those resources are now unchecked + for (const resourceType of ['alert_rule', 'alert_rule_group', 'include-all']) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).not.toBeChecked(); + } + + // Check everything again because we can + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + + // Validate that those resources are now checked again + for (const resourceType of ['alert_rule', 'alert_rule_group', 'mute_timing']) { + const checkbox = page.getByTestId( + `migrate-to-cloud-configure-snapshot-checkbox-resource-${resourceType.toLowerCase()}` + ); + await expect(checkbox).toBeChecked(); + } + + // Mock snapshot creation + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + }), + }); + }); + + await page.route(/api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [ + { + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'CREATING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + }, + ], + }), + }); + }); + + let getSnapshotCalled = false; + await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\?resultPage=1&resultLimit=50/, + async (route) => { + if (!getSnapshotCalled) { + getSnapshotCalled = true; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'CREATING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: [], + stats: { + types: {}, + statuses: {}, + total: 0, + }, + }), + }); + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'PENDING_UPLOAD', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })), + stats: STATS, + }), + }); + } + } + ); + + // Build the snapshot + const buildSnapshotButton = page.getByTestId('migrate-to-cloud-configure-snapshot-build-snapshot-button'); + await expect(buildSnapshotButton).toBeVisible(); + await buildSnapshotButton.click(); + + // Mock upload + const uploadSnapshot = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\/upload/, + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true }), + }); + } + ); + + // Upload the snapshot + const uploadButton = page.getByTestId('migrate-to-cloud-summary-upload-snapshot-button'); + await expect(uploadButton).toBeVisible(); + + await uploadButton.focus(); + await uploadButton.click({ force: true }); + + // Mock uploading status + const getSnapshotListUploading = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshots\?page=1&limit=1/, + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + snapshots: [ + { + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'UPLOADING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + }, + ], + }), + }); + } + ); + + // Simulate the snapshot being uploaded + let getSnapshotUploadingCalls = 0; + const getSnapshotUploading = await page.route( + /api\/cloudmigration\/migration\/${SESSION_UID}\/snapshot\/${SNAPSHOT_UID1}\?resultPage=1&resultLimit=50/, + async (route) => { + if (getSnapshotUploadingCalls <= 1) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: getSnapshotUploadingCalls === 1 ? 'PROCESSING' : 'UPLOADING', + created: '2025-04-02T21:40:23+02:00', + finished: '0001-01-01T00:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'PENDING' })), + stats: STATS, + }), + }); + getSnapshotUploadingCalls++; + } else { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + uid: SNAPSHOT_UID1, + sessionUid: SESSION_UID, + status: 'FINISHED', + created: '2025-03-27T12:00:00Z', + finished: '2025-03-27T12:00:00Z', + results: SNAPSHOT_RESULTS.map((r) => ({ ...r, status: 'OK' })), + stats: { + types: STATS.types, + statuses: SNAPSHOT_RESULTS.reduce( + (acc, r) => { + const status = (r as { status?: string }).status || 'UNKNOWN'; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, + {} as Record + ), + total: SNAPSHOT_RESULTS.length, + }, + }), + }); + } + } + ); + + await uploadSnapshot; + await getSnapshotListUploading; + await getSnapshotUploading; + + // At least some of the items are marked with "Uploaded to cloud" status + await expect(page.getByText('Uploaded to cloud')).toBeVisible(); + + // We can now reconfigure the snapshot + const reconfigureButton = page.getByTestId('migrate-to-cloud-summary-reconfigure-snapshot-button'); + await expect(reconfigureButton).toBeVisible(); + await reconfigureButton.click(); + + // Check the 'Include all' resources checkbox + await includeAllCheckbox.check({ force: true }); + await expect(includeAllCheckbox).toBeChecked(); + }); + }); + + test.describe('with a fake GMS backend implementation', () => { + test.afterEach(async ({ page }) => { + const disconnectButton = page.getByTestId('migrate-to-cloud-summary-disconnect-button'); + await expect(disconnectButton).toBeVisible(); + await disconnectButton.click(); + }); + + // Manually crafted base64 token for testing, does not contain any sensitive data + const TEST_TOKEN = + 'eyJUb2tlbiI6ImdsY19kZXZfZXlKdklqb2lNVEl6TkNJc0ltNGlPaUpuY21GbVlXNWhMV05zYjNWa0xXMXBaM0poZEdsdmJuTXRNVEl6TkNJc0ltc2lPaUowWlhOMElpd2liU0k2ZXlKeUlqb2laR1YyTFhWekxXTmxiblJ5WVd3aWZYMEsiLCJJbnN0YW5jZSI6eyJTdGFja0lEIjoxMjM0LCJTbHVnIjoidGVzdC1zbHVnIiwiUmVnaW9uU2x1ZyI6ImRldi11cy1jZW50cmFsIiwiQ2x1c3RlclNsdWciOiJkZXYtdXMtY2VudHJhbC0wIn19Cg=='; + + test('creates a snapshot successfully', async ({ page }) => { + // Visit the migrate to cloud onprem page + await page.goto('/admin/migrate-to-cloud'); + + // Open the connect modal and enter the token + const connectButton = page.getByTestId('migrate-to-cloud-connect-session-modal-button'); + await expect(connectButton).toBeVisible(); + await connectButton.click(); + + const tokenInput = page.getByTestId('migrate-to-cloud-connect-session-modal-token-input'); + await expect(tokenInput).toBeVisible(); + await tokenInput.focus(); + await tokenInput.fill(TEST_TOKEN); + + // Click the connect button to create the token + const connectSessionButton = page.getByTestId('migrate-to-cloud-connect-session-modal-connect-button'); + await expect(connectSessionButton).toBeVisible(); + await connectSessionButton.click(); + + // Build the snapshot + const buildSnapshotButton = page.getByTestId('migrate-to-cloud-configure-snapshot-build-snapshot-button'); + await expect(buildSnapshotButton).toBeVisible(); + await buildSnapshotButton.click(); + + // And the rebuild button should be visible + const reconfigureButton = page.getByTestId('migrate-to-cloud-summary-reconfigure-snapshot-button'); + await expect(reconfigureButton).toBeVisible(); + + // We don't upload the snapshot yet because we need to create a mock server to validate the uploaded items, + // similarly to what the SMTP (tester) server does + }); + }); + } +); diff --git a/e2e-playwright/various-suite/navigation.spec.ts b/e2e-playwright/various-suite/navigation.spec.ts new file mode 100644 index 00000000000..24defee5d6a --- /dev/null +++ b/e2e-playwright/various-suite/navigation.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Docked Navigation', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page }) => { + // This is a breakpoint where the mega menu can be docked (and docked is the default state) + await page.setViewportSize({ width: 1280, height: 800 }); + }); + + test('should remain un-docked when reloading the page', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + + // Reload the page + await page.reload(); + await expect(navMenu).toBeHidden(); + }); + + test('Can re-dock after undock', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + const openMenuButton = page.getByRole('button', { name: 'Open menu' }); + await openMenuButton.click(); + + const dockButton = page.getByRole('button', { name: 'Dock menu' }); + await dockButton.click(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeVisible(); + }); + + test('should remain in same state when navigating to another page', async ({ page, selectors }) => { + // Undock the menu + const undockButton = page.getByRole('button', { name: 'Undock menu' }); + await undockButton.click(); + + // Navigate + const openMenuButton = page.getByRole('button', { name: 'Open menu' }); + await openMenuButton.click(); + + const administrationLink = page.getByRole('link', { name: 'Administration' }); + await administrationLink.click(); + + // Still undocked + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + + // Dock the menu + await openMenuButton.click(); + const dockButton = page.getByRole('button', { name: 'Dock menu' }); + await dockButton.click(); + + // Navigate + const usersLink = page.getByRole('link', { name: 'Users' }); + await usersLink.click(); + + // Still docked + await expect(navMenu).toBeVisible(); + }); + + test('should undock on smaller viewport sizes', async ({ page, selectors }) => { + await page.setViewportSize({ width: 1120, height: 1080 }); + await page.reload(); + + const navMenu = page.getByTestId(selectors.components.NavMenu.Menu); + await expect(navMenu).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/pie-chart.spec.ts b/e2e-playwright/various-suite/pie-chart.spec.ts new file mode 100644 index 00000000000..6ead086de5b --- /dev/null +++ b/e2e-playwright/various-suite/pie-chart.spec.ts @@ -0,0 +1,21 @@ +import { selectors } from '@grafana/e2e-selectors'; +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Pie Chart Panel', + { + tag: ['@various'], + }, + () => { + test('Pie Chart rendering e2e tests', async ({ page }) => { + // Open Panel Tests - Pie Chart + await page.goto('/d/lVE-2YFMz/panel-tests-pie-chart'); + + // Check that there are 5 pie chart slices + const pieChartSlices = page.locator( + `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` + ); + await expect(pieChartSlices).toHaveCount(5); + }); + } +); diff --git a/e2e-playwright/various-suite/prometheus-annotations.spec.ts b/e2e-playwright/various-suite/prometheus-annotations.spec.ts new file mode 100644 index 00000000000..a14d054cab6 --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-annotations.spec.ts @@ -0,0 +1,137 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +import { addDashboard } from '../utils/dashboard-helpers'; +import { getResources } from '../utils/prometheus-helpers'; + +test.describe( + 'Prometheus annotations', + { + tag: ['@various'], + }, + () => { + const DATASOURCE_NAME = 'aprometheusAnnotationDS'; + + test('should navigate to variable query editor', async ({ page, selectors, createDataSourceConfigPage }) => { + const annotationName = 'promAnnotation'; + + await createDataSourceConfigPage({ type: 'prometheus', name: DATASOURCE_NAME }); + + // Add a new dashboard + await addDashboard(page); + + // Navigate to annotations + await navigateToAnnotations(page, selectors); + + // Add Prometheus annotation + await addPrometheusAnnotation(page, selectors, annotationName); + + // Open metrics browser + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + // Type in the metric selector + const selectMetricInput = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetricInput).toBeVisible(); + await selectMetricInput.fill('met'); + + // Select metric1 from the list + const metricList = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList + ); + await expect(metricList).toBeVisible(); + const metric1Option = metricList.getByText('metric1'); + await metric1Option.click(); + + // Use the query + const useQueryButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQueryButton).toBeVisible(); + await useQueryButton.click(); + + // Verify query field contains metric1 + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + await expect(queryField).toContainText('metric1'); + + // Check for other parts of the annotations + // Min step + const minStepInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.minStep); + await expect(minStepInput).toBeVisible(); + + // Title + const titleInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.title); + await titleInput.scrollIntoViewIfNeeded(); + await expect(titleInput).toBeVisible(); + + // Tags + const tagsInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.tags); + await tagsInput.scrollIntoViewIfNeeded(); + await expect(tagsInput).toBeVisible(); + + // Text + const textInput = page.getByTestId(selectors.components.DataSource.Prometheus.annotations.text); + await textInput.scrollIntoViewIfNeeded(); + await expect(textInput).toBeVisible(); + + // Series value as timestamp + const seriesValueSwitch = page.getByTestId( + selectors.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp + ); + await seriesValueSwitch.scrollIntoViewIfNeeded(); + await expect(seriesValueSwitch).toBeVisible(); + + // Go back to dashboard + const backButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.backToDashboardButton); + await expect(backButton).toBeVisible(); + await backButton.click(); + + // Check that annotation exists + await expect(page.getByText(annotationName)).toBeVisible(); + }); + + /** + * Click dashboard settings and then the annotations tab + */ + async function navigateToAnnotations(page: Page, selectors: E2ESelectorGroups) { + const editButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + const settingsButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.settingsButton); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + const annotationsTab = page.getByTestId(selectors.components.Tab.title('Annotations')); + await annotationsTab.click(); + } + + async function addPrometheusAnnotation(page: Page, selectors: E2ESelectorGroups, annotationName: string) { + const addAnnotationButton = page.getByTestId( + selectors.pages.Dashboard.Settings.Annotations.List.addAnnotationCTAV2 + ); + await addAnnotationButton.click(); + + await getResources(page); + + const nameInput = page.getByTestId(selectors.pages.Dashboard.Settings.Annotations.Settings.name); + await nameInput.clear(); + await nameInput.fill(annotationName); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(DATASOURCE_NAME); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + } + } +); diff --git a/e2e-playwright/various-suite/prometheus-config.spec.ts b/e2e-playwright/various-suite/prometheus-config.spec.ts new file mode 100644 index 00000000000..a92c9b7c2bd --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-config.spec.ts @@ -0,0 +1,193 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +// Todo: Fix datasource creation +test.describe.skip( + 'Prometheus config', + { + tag: ['@various'], + }, + () => { + const DATASOURCE_ID = 'Prometheus'; + const DATASOURCE_TYPED_NAME = 'PrometheusDatasourceInstance'; + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + // Navigate to add data source page + await page.goto('/datasources/new'); + + // Select the Prometheus data source + const prometheusPlugin = page.getByRole('button', { name: DATASOURCE_ID }); + await prometheusPlugin.scrollIntoViewIfNeeded(); + await expect(prometheusPlugin).toBeVisible(); + await prometheusPlugin.click(); + }); + + test('should have the following components: connection settings, managed alerts, scrape interval, query timeout, default editor, disable metric lookup, prometheus type, cache level, incremental querying, disable recording rules, custom query parameters, http method', async ({ + page, + selectors, + }) => { + // connection settings + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await expect(connectionSettings).toBeVisible(); + + // managed alerts + const manageAlerts = page.locator(`#${selectors.components.DataSource.Prometheus.configPage.manageAlerts}`); + await manageAlerts.scrollIntoViewIfNeeded(); + await expect(manageAlerts).toBeVisible(); + + // scrape interval + const scrapeInterval = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.scrapeInterval); + await scrapeInterval.scrollIntoViewIfNeeded(); + await expect(scrapeInterval).toBeVisible(); + + // query timeout + const queryTimeout = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.queryTimeout); + await queryTimeout.scrollIntoViewIfNeeded(); + await expect(queryTimeout).toBeVisible(); + + // default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + + // disable metric lookup + const disableMetricLookup = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.disableMetricLookup}` + ); + await disableMetricLookup.scrollIntoViewIfNeeded(); + await expect(disableMetricLookup).toBeVisible(); + + // prometheus type + const prometheusType = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.prometheusType); + await prometheusType.scrollIntoViewIfNeeded(); + await expect(prometheusType).toBeVisible(); + + // cache level + const cacheLevel = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.cacheLevel); + await cacheLevel.scrollIntoViewIfNeeded(); + await expect(cacheLevel).toBeVisible(); + + // incremental querying + const incrementalQuerying = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}` + ); + await incrementalQuerying.scrollIntoViewIfNeeded(); + await expect(incrementalQuerying).toBeVisible(); + + // disable recording rules + const disableRecordingRules = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.disableRecordingRules}` + ); + await disableRecordingRules.scrollIntoViewIfNeeded(); + await expect(disableRecordingRules).toBeVisible(); + + // custom query parameters + const customQueryParameters = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.customQueryParameters + ); + await customQueryParameters.scrollIntoViewIfNeeded(); + await expect(customQueryParameters).toBeVisible(); + + // http method + const httpMethod = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.httpMethod); + await httpMethod.scrollIntoViewIfNeeded(); + await expect(httpMethod).toBeVisible(); + }); + + test('should save the default editor when navigating to explore', async ({ page, selectors }) => { + // Click on default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + await defaultEditor.click(); + + // Select 'Builder' option + await selectOption(page, 'Builder'); + + // Set connection settings + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.fill('http://prom-url:9090'); + + // Set data source name + const nameInput = page.getByTestId(selectors.pages.DataSource.name); + await nameInput.clear(); + await nameInput.fill(DATASOURCE_TYPED_NAME); + + // Save and test + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + + // Navigate to explore + await page.goto('/explore'); + + // Select the data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + // Type the data source name and press enter + await page.keyboard.type(DATASOURCE_TYPED_NAME); + await page.keyboard.press('Enter'); + + // Verify the builder metric select is visible + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + }); + + test('should allow a user to add the version when the Prom type is selected', async ({ page, selectors }) => { + // Click on prometheus type + const prometheusType = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.prometheusType); + await prometheusType.scrollIntoViewIfNeeded(); + await expect(prometheusType).toBeVisible(); + await prometheusType.click(); + + // Select 'Prometheus' option + await selectOption(page, 'Prometheus'); + + // Verify prometheus version is visible + const prometheusVersion = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.prometheusVersion + ); + await prometheusVersion.scrollIntoViewIfNeeded(); + await expect(prometheusVersion).toBeVisible(); + }); + + test('should have a cache level component', async ({ page, selectors }) => { + // Verify cache level is visible + const cacheLevel = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.cacheLevel); + await cacheLevel.scrollIntoViewIfNeeded(); + await expect(cacheLevel).toBeVisible(); + }); + + test('should allow a user to select a query overlap window when incremental querying is selected', async ({ + page, + selectors, + }) => { + // Check the incremental querying checkbox + const incrementalQuerying = page.locator( + `#${selectors.components.DataSource.Prometheus.configPage.incrementalQuerying}` + ); + await incrementalQuerying.scrollIntoViewIfNeeded(); + await expect(incrementalQuerying).toBeVisible(); + await incrementalQuerying.check({ force: true }); + + // Verify query overlap window is visible + const queryOverlapWindow = page.getByTestId( + selectors.components.DataSource.Prometheus.configPage.queryOverlapWindow + ); + await queryOverlapWindow.scrollIntoViewIfNeeded(); + await expect(queryOverlapWindow).toBeVisible(); + }); + } +); + +async function selectOption(page, option) { + const optionElement = page.getByRole('option').filter({ hasText: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/prometheus-editor.spec.ts b/e2e-playwright/various-suite/prometheus-editor.spec.ts new file mode 100644 index 00000000000..2aafd3cb9aa --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-editor.spec.ts @@ -0,0 +1,287 @@ +import { Page } from 'playwright-core'; + +import { test, expect, E2ESelectorGroups } from '@grafana/plugin-e2e'; + +import { getResources } from '../utils/prometheus-helpers'; + +// TODO: fix some tests. Race conditions with other tests in the file cause some to fail. +test.describe.skip( + 'Prometheus query editor', + { + tag: ['@various', '@wip'], + }, + () => { + const DATASOURCE_ID = 'Prometheus'; + + type EditorType = 'Code' | 'Builder'; + + /** + * Create and save a Prometheus data source, navigate to code or builder + */ + async function navigateToEditor(page: Page, selectors: E2ESelectorGroups, editorType: string, name: string) { + // Navigate to add data source page + await page.goto('/datasources/new'); + + // Select the Prometheus data source + const prometheusPlugin = page.getByRole('button', { name: DATASOURCE_ID }); + await prometheusPlugin.scrollIntoViewIfNeeded(); + await expect(prometheusPlugin).toBeVisible(); + await prometheusPlugin.click(); + + // Choose default editor + const defaultEditor = page.getByTestId(selectors.components.DataSource.Prometheus.configPage.defaultEditor); + await defaultEditor.scrollIntoViewIfNeeded(); + await expect(defaultEditor).toBeVisible(); + await defaultEditor.click(); + + await selectOption(page, editorType, selectors); + + // Add URL for DS to save without error + const connectionSettings = page.getByLabel( + selectors.components.DataSource.Prometheus.configPage.connectionSettings + ); + await connectionSettings.fill('http://prom-url:9090'); + + // Name the DS + const nameInput = page.getByTestId(selectors.pages.DataSource.name); + await nameInput.clear(); + await nameInput.fill(name); + + const saveAndTestButton = page.getByTestId(selectors.pages.DataSource.saveAndTest); + await saveAndTestButton.click(); + + // Visit explore + await page.goto('/explore'); + + // Choose the right DS + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByRole('button', { name: `${name} ${DATASOURCE_ID}`, exact: true }); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + } + + test('should have a kickstart component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const queryPatterns = page.getByTestId(selectors.components.QueryBuilder.queryPatterns); + await queryPatterns.scrollIntoViewIfNeeded(); + await expect(queryPatterns).toBeVisible(); + }); + + test('should have an explain component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const explain = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.explain); + await explain.scrollIntoViewIfNeeded(); + await expect(explain).toBeVisible(); + }); + + test('should have an editor toggle component', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + const editorToggle = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.editorToggle); + await editorToggle.scrollIntoViewIfNeeded(); + await expect(editorToggle).toBeVisible(); + }); + + test('should have an options component with legend, format, step, type and exemplars', async ({ + page, + selectors, + }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheus'); + + // Open options + const options = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.options); + await options.scrollIntoViewIfNeeded(); + await expect(options).toBeVisible(); + await options.click(); + + // Check options + const legend = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.legend); + await legend.scrollIntoViewIfNeeded(); + await expect(legend).toBeVisible(); + + const format = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.format); + await format.scrollIntoViewIfNeeded(); + await expect(format).toBeVisible(); + + const step = page.locator('[data-test-id="prometheus-step"]'); + await step.scrollIntoViewIfNeeded(); + await expect(step).toBeVisible(); + + const type = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.type); + await type.scrollIntoViewIfNeeded(); + await expect(type).toBeVisible(); + + const exemplars = page.getByTestId('prometheus-exemplars'); + await exemplars.scrollIntoViewIfNeeded(); + await expect(exemplars).toBeVisible(); + }); + + test.describe('Code editor', () => { + test('navigates to the code editor with editor type as code', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + }); + + test('navigates to the code editor and opens the metrics browser with metric search, labels, label values, and all components', async ({ + page, + selectors, + }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + + await getResources(page); + + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + const selectMetric = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetric).toBeVisible(); + + const labelNamesFilter = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelNamesFilter + ); + await expect(labelNamesFilter).toBeVisible(); + + const labelValuesFilter = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelValuesFilter + ); + await expect(labelValuesFilter).toBeVisible(); + + const useQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQuery).toBeVisible(); + + const useAsRateQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useAsRateQuery + ); + await expect(useAsRateQuery).toBeVisible(); + + const validateSelector = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.validateSelector + ); + await expect(validateSelector).toBeVisible(); + + const clear = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.clear + ); + await expect(clear).toBeVisible(); + }); + + test('selects a metric in the metrics browser and uses the query', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Code', 'prometheusCode'); + + await getResources(page); + + const metricsBrowserButton = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton + ); + await metricsBrowserButton.click(); + + const selectMetric = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric + ); + await expect(selectMetric).toBeVisible(); + await selectMetric.fill('met'); + + const metricList = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.metricList + ); + await expect(metricList).toBeVisible(); + + const metricOption = metricList.getByText('metric1'); + await expect(metricOption).toBeVisible(); + await metricOption.click(); + + const useQuery = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.useQuery + ); + await expect(useQuery).toBeVisible(); + await useQuery.click(); + + const queryField = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.code.queryField); + await expect(queryField).toBeVisible(); + await expect(queryField).toContainText('metric1'); + }); + }); + + test.describe('Query builder', () => { + test('navigates to the query builder with editor type as code', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + }); + + test('the query builder contains metric select, label filters and operations', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + + const labelSelect = page.getByTestId(selectors.components.QueryBuilder.labelSelect); + await expect(labelSelect).toBeVisible(); + + const matchOperatorSelect = page.getByTestId(selectors.components.QueryBuilder.matchOperatorSelect); + await expect(matchOperatorSelect).toBeVisible(); + + const valueSelect = page.getByTestId(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + }); + + test('can select a metric and provide a hint', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + + await page.getByText('metric1').click(); + + const hints = page.getByTestId(selectors.components.DataSource.Prometheus.queryEditor.builder.hints); + await expect(hints).toContainText('hint: add rate'); + }); + + test('should have the metrics explorer opened via the metric select', async ({ page, selectors }) => { + await navigateToEditor(page, selectors, 'Builder', 'prometheusBuilder'); + + await getResources(page); + + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + + await selectOption(page, 'Metrics explorer', selectors); + + const metricsExplorer = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricsExplorer + ); + await expect(metricsExplorer).toBeVisible(); + }); + }); + } +); + +async function selectOption(page: Page, option: string, selectors: E2ESelectorGroups) { + const optionElement = page.getByTestId(selectors.components.Select.option).filter({ hasText: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts new file mode 100644 index 00000000000..a2b4dfcd672 --- /dev/null +++ b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts @@ -0,0 +1,171 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import { addDashboard } from '../utils/dashboard-helpers'; +import { getResources } from '../utils/prometheus-helpers'; + +test.describe.skip( + 'Prometheus variable query editor', + { + tag: ['@various', '@wip'], + }, + () => { + const DATASOURCE_NAME = 'prometheusVariableDS'; + + /** + * Click dashboard settings and then the variables tab + */ + async function navigateToVariables(page, selectors) { + const editButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.editButton); + await expect(editButton).toBeVisible(); + await editButton.click(); + + const settingsButton = page.getByTestId(selectors.components.NavToolbar.editDashboard.settingsButton); + await expect(settingsButton).toBeVisible(); + await settingsButton.click(); + + const variablesTab = page.getByTestId(selectors.components.Tab.title('Variables')); + await variablesTab.click(); + } + + /** + * Begin the process of adding a query type variable for a Prometheus data source + */ + async function addPrometheusQueryVariable(page, selectors, variableName) { + const addVariableButton = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.addVariableCTAV2); + await addVariableButton.click(); + + const nameInput = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2); + await nameInput.clear(); + await nameInput.fill(variableName); + + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + const dataSourceOption = page.getByText(DATASOURCE_NAME); + await dataSourceOption.scrollIntoViewIfNeeded(); + await expect(dataSourceOption).toBeVisible(); + await dataSourceOption.click(); + + await getResources(page); + } + + /** + * Create a Prometheus variable and navigate to the query editor to check that it is available to use. + */ + async function variableFlowToQueryEditor(page, selectors, variableName, queryType) { + await addDashboard(page); + await navigateToVariables(page, selectors); + await addPrometheusQueryVariable(page, selectors, variableName); + + // Select query type + const queryTypeSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.variableQueryEditor.queryType + ); + await queryTypeSelect.click(); + await selectOption(page, queryType); + + // Apply the variable + const applyButton = page.getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.applyButton); + await applyButton.click(); + + // Close to return to dashboard + const backToDashboardButton = page.getByTestId( + selectors.components.NavToolbar.editDashboard.backToDashboardButton + ); + await expect(backToDashboardButton).toBeVisible(); + await backToDashboardButton.click(); + + // Add visualization + const createNewPanelButton = page.getByTestId(selectors.pages.AddDashboard.itemButton('Create new panel button')); + await expect(createNewPanelButton).toBeVisible(); + await createNewPanelButton.click(); + + // Close the data source picker modal + const closeButton = page.getByRole('button', { name: 'Close menu' }); + await closeButton.click({ force: true }); + + // Select prom data source from the data source list + const dataSourcePickerInput = page.getByTestId(selectors.components.DataSourcePicker.inputV2); + await dataSourcePickerInput.click(); + await dataSourcePickerInput.fill(DATASOURCE_NAME); + await page.keyboard.press('Enter'); + + // Confirm the variable exists in the correct input + switch (queryType) { + case 'Label names': + const labelSelect = page.getByTestId(selectors.components.QueryBuilder.labelSelect); + await expect(labelSelect).toBeVisible(); + await labelSelect.click(); + await selectOption(page, variableName); + break; + case 'Label values': + const valueSelect = page.getByTestId(selectors.components.QueryBuilder.valueSelect); + await expect(valueSelect).toBeVisible(); + await valueSelect.click(); + await selectOption(page, variableName); + break; + case 'Metrics': + const metricSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.queryEditor.builder.metricSelect + ); + await expect(metricSelect).toBeVisible(); + await metricSelect.click(); + await selectOption(page, variableName); + break; + default: + // do nothing + break; + } + } + + test.beforeEach(async ({ page, selectors, createDataSourceConfigPage }) => { + await createDataSourceConfigPage({ type: 'prometheus', name: DATASOURCE_NAME }); + }); + + test('should navigate to variable query editor', async ({ page, selectors }) => { + await addDashboard(page); + await navigateToVariables(page, selectors); + }); + + test('should select a query type for a Prometheus variable query', async ({ page, selectors }) => { + await addDashboard(page); + await navigateToVariables(page, selectors); + await addPrometheusQueryVariable(page, selectors, 'labelsVariable'); + + // Select query type + const queryTypeSelect = page.getByTestId( + selectors.components.DataSource.Prometheus.variableQueryEditor.queryType + ); + await queryTypeSelect.click(); + await selectOption(page, 'Label names'); + }); + + test('should create a label names variable that is selectable in the label select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'labelnames', 'Label names'); + }); + + test('should create a label values variable that is selectable in the label values select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'labelvalues', 'Label values'); + }); + + test('should create a metric names variable that is selectable in the metric select in query builder', async ({ + page, + selectors, + }) => { + await variableFlowToQueryEditor(page, selectors, 'metrics', 'Metrics'); + }); + } +); + +async function selectOption(page, option) { + const optionElement = page.getByRole('option', { name: option }); + await expect(optionElement).toBeVisible(); + await optionElement.click(); +} diff --git a/e2e-playwright/various-suite/query-editor.spec.ts b/e2e-playwright/various-suite/query-editor.spec.ts new file mode 100644 index 00000000000..ee1e4e3e372 --- /dev/null +++ b/e2e-playwright/various-suite/query-editor.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Query editor', + { + tag: ['@various'], + }, + () => { + test('Undo should work in query editor for prometheus', async ({ page }) => { + // Visit the explore page + await page.goto('/explore'); + + // Click on the data source picker + const dataSourcePicker = page.getByTestId('data-testid Data source picker select container'); + await expect(dataSourcePicker).toBeVisible(); + await dataSourcePicker.click(); + + // Select the prometheus data source + const prometheusOption = page.getByText('gdev-prometheus'); + await expect(prometheusOption).toBeVisible(); + await prometheusOption.click(); + + const queryText = `rate(http_requests_total{job="grafana"}[5m])`; + + // Click on the Code radio button + const codeRadioButton = page.getByRole('radio', { name: 'Code' }); + await expect(codeRadioButton).toBeVisible(); + await codeRadioButton.click(); + + // Wait for Monaco editor to load + await page.waitForSelector('.monaco-editor'); + + // Type the query text and then backspace + const queryField = page.locator('.monaco-editor textarea'); + await queryField.fill(queryText); + await queryField.press('Backspace'); + + // Verify the text is truncated + await expect(page.getByText(queryText.slice(0, -1))).toBeVisible(); + + // Use undo (Ctrl+Z) + await queryField.press('Control+z'); + + // Verify the full query text is restored + await expect(page.getByText(queryText)).toBeVisible(); + + // Verify no error alerts are visible + const errorAlert = page.getByTestId('alert-error'); + await expect(errorAlert).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/return-to-previous.spec.ts b/e2e-playwright/various-suite/return-to-previous.spec.ts new file mode 100644 index 00000000000..9c6b9f7748e --- /dev/null +++ b/e2e-playwright/various-suite/return-to-previous.spec.ts @@ -0,0 +1,133 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'ReturnToPrevious button', + { + tag: ['@various'], + }, + () => { + test.beforeEach(async ({ page, selectors }) => { + // Navigate to alerting list + await page.goto('/alerting/list'); + + // Click on the first group toggle to expand it + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.first().click(); + + // Click on the toggle to expand the content + const toggle = page.getByTestId(selectors.components.AlertRules.toggle); + await toggle.click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // Store the alert rule URL for later comparison + const alertRuleUrl = page.url(); + + // Click on "View panel" link + const viewPanelLink = page.getByRole('link', { name: 'View panel' }); + await viewPanelLink.click(); + + // Store the URL for use in tests + test.info().annotations.push({ + type: 'alertRuleUrl', + description: alertRuleUrl, + }); + }); + + test('should appear when changing context and go back to alert rule when clicking "Back"', async ({ + page, + selectors, + }) => { + // Check whether all elements of RTP are available + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeVisible(); + + const dismissButton = page.getByTestId(selectors.components.ReturnToPrevious.dismissButton); + await expect(dismissButton).toBeVisible(); + + const backButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(backButton).toBeVisible(); + + // Check that the button contains the expected text + await expect(backButton.getByText('Back to e2e-ReturnToPrevious-test')).toBeVisible(); + + // Click the back button + await backButton.click(); + + // Check whether the RTP button leads back to alert rule + const alertRuleUrl = test.info().annotations.find((a) => a.type === 'alertRuleUrl')?.description; + expect(page.url()).toBe(alertRuleUrl); + }); + + test('should disappear when clicking "Dismiss"', async ({ page, selectors }) => { + const dismissButton = page.getByTestId(selectors.components.ReturnToPrevious.dismissButton); + await expect(dismissButton).toBeVisible(); + await dismissButton.click(); + + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeHidden(); + }); + + test('should not persist when going back to the alert rule details view', async ({ page, selectors }) => { + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeVisible(); + + // Navigate back to alerting list + await page.goto('/alerting/list'); + + // Click on the first group toggle + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.first().click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // The ReturnToPrevious button should not exist + const rtpButtonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(rtpButtonGroup).toBeHidden(); + }); + + test('should override the button label and change the href when user changes alert rules', async ({ + page, + selectors, + }) => { + const backButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(backButton.getByText('Back to e2e-ReturnToPrevious-test')).toBeVisible(); + + // Navigate back to alerting list + await page.goto('/alerting/list'); + + // Click on the last group toggle (different alert rule) + const groupToggle = page.getByTestId(selectors.components.AlertRules.groupToggle); + await groupToggle.last().click(); + + // Click on the "View" link + const viewLink = page.getByRole('link', { name: 'View' }); + await viewLink.click(); + + // Store the second alert rule URL + const alertRule2Url = page.url(); + + // Click on "View panel" link + const viewPanelLink = page.getByRole('link', { name: 'View panel' }); + await viewPanelLink.click(); + + // Check that the button now shows the new alert rule name + const newBackButton = page.getByTestId(selectors.components.ReturnToPrevious.backButton); + await expect(newBackButton.getByText('Back to e2e-ReturnToPrevious-test-2')).toBeVisible(); + + // Click the back button + await newBackButton.click(); + + // The ReturnToPrevious button should disappear + const buttonGroup = page.getByTestId(selectors.components.ReturnToPrevious.buttonGroup); + await expect(buttonGroup).toBeHidden(); + + // Check whether the RTP button leads back to the second alert rule + expect(page.url()).toBe(alertRule2Url); + }); + } +); diff --git a/e2e-playwright/various-suite/solo-route.spec.ts b/e2e-playwright/various-suite/solo-route.spec.ts new file mode 100644 index 00000000000..81c9e8baf7b --- /dev/null +++ b/e2e-playwright/various-suite/solo-route.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Solo Route', + { + tag: ['@various'], + }, + () => { + test('Can view panels with shared queries in fullscreen', async ({ page, selectors }) => { + // open Panel Tests - Bar Gauge + const soloPanelUrl = selectors.pages.SoloPanel.url('ZqZnVvFZz/datasource-tests-shared-queries?orgId=1&panelId=4'); + await page.goto(soloPanelUrl); + + // Check that there are 6 canvas elements + const canvasElements = page.locator('canvas'); + await expect(canvasElements).toHaveCount(6); + }); + + test('Can view solo panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'TkZXxlNG3/panel-tests-graph-ng?orgId=1&from=1699954597665&to=1699956397665&panelId=54&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('Interpolation: Step before')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + + test('Can view solo repeated panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server=A')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + + test('Can view solo in repeated row and panel in scenes', async ({ page, selectors }) => { + // open Panel Tests - Graph NG + const soloPanelUrl = selectors.pages.SoloPanel.url( + 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' + ); + await page.goto(soloPanelUrl); + + // Check that the panel title exists + const panelTitle = page.getByTestId(selectors.components.Panels.Panel.title('server = B, pod = Rob')); + await expect(panelTitle).toBeVisible(); + + // Check that uplot-main-div does not exist + const uplotDiv = page.getByText('uplot-main-div'); + await expect(uplotDiv).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/various-suite/trace-view-scrolling.spec.ts b/e2e-playwright/various-suite/trace-view-scrolling.spec.ts new file mode 100644 index 00000000000..4343ed5eb13 --- /dev/null +++ b/e2e-playwright/various-suite/trace-view-scrolling.spec.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { test, expect } from '@grafana/plugin-e2e'; + +// this test requires a larger viewport +test.use({ + viewport: { width: 1280, height: 1080 }, +}); + +// TODO for some reason, this test gives "connection refused" errors in CI +test.describe.skip( + 'Trace view', + { + tag: ['@various'], + }, + () => { + test('Can lazy load big traces', async ({ page, selectors }) => { + // Load the fixture data + const fixturePath = join(__dirname, '../fixtures/long-trace-response.json'); + const longTraceResponse = JSON.parse(readFileSync(fixturePath, 'utf8')); + + // Mock the API response + await page.route('*/**/api/traces/trace', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(longTraceResponse), + }); + }); + + // Navigate to Explore page + await page.goto(selectors.pages.Explore.url); + + // Select the Jaeger data source + const dataSourcePicker = page.getByTestId(selectors.components.DataSourcePicker.container); + await dataSourcePicker.click(); + const datasourceList = page.getByTestId(selectors.components.DataSourcePicker.dataSourceList); + await datasourceList.getByText('gdev-jaeger').click(); + + // Check that gdev-jaeger is visible in the query editor + await expect(page.getByText('gdev-jaeger')).toBeVisible(); + + // Type the query + const queryField = page + .getByTestId(selectors.components.QueryField.container) + .locator('[contenteditable="true"]'); + await queryField.fill('trace'); + + // Use Shift+Enter to execute the query + await queryField.press('Shift+Enter'); + + // Get the initial count of span bars + const initialSpanBars = page.getByTestId(selectors.components.TraceViewer.spanBar); + const initialSpanBarCount = await initialSpanBars.count(); + + await initialSpanBars.last().scrollIntoViewIfNeeded(); + await expect + .poll(async () => await page.getByTestId(selectors.components.TraceViewer.spanBar).count()) + .toBeGreaterThan(initialSpanBarCount); + }); + } +); diff --git a/e2e-playwright/various-suite/verify-i18n.spec.ts b/e2e-playwright/various-suite/verify-i18n.spec.ts new file mode 100644 index 00000000000..f6a2a00eb9b --- /dev/null +++ b/e2e-playwright/various-suite/verify-i18n.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +const I18N_USER = 'i18n-test'; +const I18N_PASSWORD = 'i18n-test'; + +// Separate user to isolate changes from other tests +test.use({ + user: { + user: I18N_USER, + password: I18N_PASSWORD, + }, + storageState: { + cookies: [], + origins: [], + }, +}); + +test.describe( + 'Verify i18n', + { + tag: ['@various'], + }, + () => { + // Map between languages in the language picker and the corresponding translation of the 'Language' label + const languageMap: Record = { + Deutsch: 'Sprache', + Español: 'Idioma', + Français: 'Langue', + 'Português Brasileiro': 'Idioma', + '中文(简体)': '语言', + English: 'Language', + }; + + // Basic test which loops through the defined languages in the picker + // and verifies that the corresponding label is translated correctly + test('loads all the languages correctly', async ({ page, selectors, createUser }) => { + await createUser(); + // login manually for now + await page.getByTestId(selectors.pages.Login.username).fill(I18N_USER); + await page.getByTestId(selectors.pages.Login.password).fill(I18N_PASSWORD); + await page.getByTestId(selectors.pages.Login.submit).click(); + await expect(page.getByTestId(selectors.components.NavToolbar.commandPaletteTrigger)).toBeVisible(); + + // Navigate to profile page + await page.goto('/profile'); + + const LANGUAGE_SELECTOR = '[id="language-preference-select"]'; + + // Loop through each language and test the translation + for (const [language, label] of Object.entries(languageMap)) { + // Check that the language selector is not disabled + const languageSelector = page.locator(LANGUAGE_SELECTOR); + await expect(languageSelector).not.toBeDisabled(); + + // Click on the language selector + await languageSelector.click(); + + // Clear and type the language name + await languageSelector.clear(); + await languageSelector.fill(language); + + // Press down arrow and enter to select the option + await languageSelector.press('ArrowDown'); + await languageSelector.press('Enter'); + + // Click the save preferences button + const saveButton = page.getByTestId(selectors.components.UserProfile.preferencesSaveButton); + await saveButton.click(); + + // Check that the language label is visible + const languageLabel = page.locator('label').filter({ hasText: label }); + await expect(languageLabel).toBeVisible(); + + // Verify the language selector has the correct value + await expect(languageSelector).toHaveValue(language); + } + }); + } +); diff --git a/e2e-playwright/various-suite/visualization-suggestions.spec.ts b/e2e-playwright/various-suite/visualization-suggestions.spec.ts new file mode 100644 index 00000000000..964af821caa --- /dev/null +++ b/e2e-playwright/various-suite/visualization-suggestions.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +test.describe( + 'Visualization suggestions', + { + tag: ['@various'], + }, + () => { + test('Should be shown and clickable', async ({ page, selectors, gotoPanelEditPage }) => { + // Open dashboard with edit panel + const panelEditPage = await gotoPanelEditPage({ + dashboard: { + uid: 'aBXrJ0R7z', + }, + id: '9', + }); + + // Try visualization suggestions + await panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await panelEditPage + .getByGrafanaSelector(selectors.components.RadioButton.container) + .filter({ hasText: 'Suggestions' }) + .click(); + + // Verify we see suggestions + const lineChartCard = panelEditPage.getByGrafanaSelector( + selectors.components.VisualizationPreview.card('Line chart') + ); + await expect(lineChartCard).toBeVisible(); + + // Verify search works + const searchInput = page.getByPlaceholder('Search for...'); + await searchInput.fill('Table'); + + // Should no longer see line chart + await expect(lineChartCard).toBeHidden(); + + // Select a visualization + await panelEditPage.getByGrafanaSelector(selectors.components.VisualizationPreview.card('Table')).click(); + + // Verify table header is visible + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.header) + ).toBeVisible(); + }); + } +); diff --git a/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts b/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts deleted file mode 100644 index bb1ccee1f72..00000000000 --- a/e2e/plugin-e2e/azuremonitor/azuremonitor.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'grafana-azure-monitor-datasource' }); - - await expect(await page.getByText('Type: Azure Monitor', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Authentication', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts b/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts deleted file mode 100644 index db3ab66e985..00000000000 --- a/e2e/plugin-e2e/cloudmonitoring/cloudmonitoring.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'stackdriver' }); - - await expect(await page.getByText('Type: Google Cloud Monitoring', { exact: true })).toBeVisible(); - await expect(await page.getByText('Google JWT File', { exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts b/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts deleted file mode 100644 index e834252208d..00000000000 --- a/e2e/plugin-e2e/cloudwatch/cloudwatch.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'cloudwatch' }); - - await expect(await page.getByText('Type: CloudWatch', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection Details', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts b/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts deleted file mode 100644 index f1232c11d0f..00000000000 --- a/e2e/plugin-e2e/elasticsearch/elasticsearch.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'elasticsearch' }); - - await expect(await page.getByText('Type: Elasticsearch', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/graphite/graphite.spec.ts b/e2e/plugin-e2e/graphite/graphite.spec.ts deleted file mode 100644 index 9bf58625c4b..00000000000 --- a/e2e/plugin-e2e/graphite/graphite.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'graphite' }); - - await expect(await page.getByText('Type: Graphite', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/influxdb/influxdb.spec.ts b/e2e/plugin-e2e/influxdb/influxdb.spec.ts deleted file mode 100644 index 1538690c9df..00000000000 --- a/e2e/plugin-e2e/influxdb/influxdb.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'influxdb' }); - - await expect(await page.getByText('Type: InfluxDB', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/jaeger/jaeger.spec.ts b/e2e/plugin-e2e/jaeger/jaeger.spec.ts deleted file mode 100644 index c7e1bc88951..00000000000 --- a/e2e/plugin-e2e/jaeger/jaeger.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'jaeger' }); - - await expect(await page.getByText('Type: Jaeger', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mssql/mssql.spec.ts b/e2e/plugin-e2e/mssql/mssql.spec.ts deleted file mode 100644 index 125024935ea..00000000000 --- a/e2e/plugin-e2e/mssql/mssql.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'mssql' }); - - await expect(await page.getByText('Type: Microsoft SQL Server', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mysql/mysql.spec.ts b/e2e/plugin-e2e/mysql/mysql.spec.ts deleted file mode 100644 index 525e0074918..00000000000 --- a/e2e/plugin-e2e/mysql/mysql.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { tableNameWithSpecialCharacter } from './mocks/mysql.mocks'; -import { mockDataSourceRequest } from './utils'; - -test.beforeEach(mockDataSourceRequest); - -test('code editor autocomplete should handle table name escaping/quoting', async ({ explorePage, selectors, page }) => { - await page.getByLabel('Code').check(); - - const editor = explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox'); - await editor.fill('S'); - await page.getByLabel('SELECT FROM
').locator('a').click(); - await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); - await page.keyboard.press('Enter'); - - await expect(editor).toHaveValue(`SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``); - - for (let i = 0; i < tableNameWithSpecialCharacter.length + 2; i++) { - await page.keyboard.press('Backspace'); - } - - await page.keyboard.press('Control+I'); - await expect(page.getByLabel(tableNameWithSpecialCharacter)).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts b/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts deleted file mode 100644 index 68450533ee1..00000000000 --- a/e2e/plugin-e2e/mysql/visual-query-builder.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { selectors } from '@grafana/e2e-selectors'; -import { test, expect } from '@grafana/plugin-e2e'; - -import { normalTableName } from './mocks/mysql.mocks'; -import { mockDataSourceRequest } from './utils'; - -test.beforeEach(mockDataSourceRequest); - -test('visual query builder should handle macros', async ({ explorePage, page }) => { - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); - await page.getByText(normalTableName, { exact: true }).click(); - - // Open Data operations - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).click(); - const select = page.getByLabel('Select options menu'); - await select.locator(page.getByText('$__timeGroupAlias')).click(); - - // Open column selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')).click(); - await select.locator(page.getByText('createdAt')).click(); - - // Open Interval selector - await explorePage - .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Interval')) - .click(); - await select.locator(page.getByText('$__interval')).click(); - - await page.getByRole('button', { name: 'Add column' }).click(); - - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectAggregation).nth(1).click(); - await select.locator(page.getByText('AVG')).click(); - - await explorePage - .getByGrafanaSelector(selectors.components.SQLQueryEditor.selectFunctionParameter('Column')) - .nth(1) - .click(); - await select.locator(page.getByText('bigint')).click(); - - // Validate the query - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue( - `SELECT\n $__timeGroupAlias(createdAt, $__interval),\n AVG(\`bigint\`)\nFROM\n DataMaker.normalTable\nLIMIT\n 50` - ); -}); - -test('visual query builder should handle time filter macro', async ({ explorePage, page }) => { - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.headerTableSelector).click(); - await page.getByText(normalTableName, { exact: true }).click(); - - // Open column selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.selectColumn).click(); - const select = page.getByLabel('Select options menu'); - await select.locator(page.getByText('createdAt')).click(); - - // Toggle where row - await page.getByLabel('Filter').last().click(); - - // Click add filter button - await page.getByRole('button', { name: 'Add filter' }).click(); - await page.getByRole('button', { name: 'Add filter' }).click(); // For some reason we need to click twice - - // Open field selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterField).click(); - await select.locator(page.getByText('createdAt')).click(); - - // Open operator selector - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); - await select.locator(page.getByText('Macros')).click(); - - // Open macros value selector - await explorePage.getByGrafanaSelector('Macros value selector').click(); - await select.locator(page.getByText('timeFilter', { exact: true })).click(); - - // Validate that the timeFilter macro was added - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n $__timeFilter(createdAt)\nLIMIT\n 50`); - - // Validate that the timeFilter macro was removed when changed to equals operator - await explorePage.getByGrafanaSelector(selectors.components.SQLQueryEditor.filterOperator).click(); - await select.locator(page.getByText('==')).click(); - - await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).click(); - await explorePage.getByGrafanaSelector(selectors.components.DateTimePicker.input).blur(); - - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).not.toHaveValue(`SELECT\n createdAt\nFROM\n DataMaker.normalTable\nWHERE\n createdAt = NULL\nLIMIT\n 50`); -}); - -test('visual query builder should not crash when filter is set to select_any_in', async ({ explorePage, page }) => { - const queryParams = new URLSearchParams(); - queryParams.set('schemaVersion', '1'); - queryParams.set('orgId', '1'); - const panes = { - mmm: { - datasource: 'P4FDCC188E688367F', - queries: [ - { - refId: 'A', - datasource: { - type: 'mysql', - uid: 'P4FDCC188E688367F', - }, - format: 'table', - rawSql: "SELECT * FROM DataMaker.normalTable WHERE name IN ('a') LIMIT 50 ", - editorMode: 'builder', - sql: { - columns: [ - { - type: 'function', - parameters: [ - { - type: 'functionParameter', - name: '*', - }, - ], - }, - ], - groupBy: [ - { - type: 'groupBy', - property: { - type: 'string', - }, - }, - ], - limit: 50, - whereJsonTree: { - id: 'baa99aa9-0123-4456-b89a-b195d1dcfc6a', - type: 'group', - children1: [ - { - type: 'rule', - id: 'bb9a8bba-89ab-4cde-b012-3195d1dd2c91', - properties: { - fieldSrc: 'field', - field: 'name', - operator: 'select_any_in', - value: ['a'], - valueSrc: ['value'], - valueType: ['text'], - }, - }, - ], - }, - whereString: "name IN ('a')", - }, - dataset: 'DataMaker', - table: 'normalTable', - }, - ], - }, - }; - queryParams.set('panes', JSON.stringify(panes)); - - await explorePage.goto({ queryParams }); - - // Validate the query - await expect( - explorePage.getByGrafanaSelector(selectors.components.CodeEditor.container).getByRole('textbox') - ).toHaveValue(`SELECT\n *\nFROM\n DataMaker.normalTable\nWHERE\n name IN ('a')\nLIMIT\n 50`); -}); diff --git a/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts b/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts deleted file mode 100644 index c5e38fcbd10..00000000000 --- a/e2e/plugin-e2e/opentsdb/opentsdb.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: decoupled frontend plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'opentsdb' }); - - await expect(await page.getByText('Type: OpenTSDB', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'HTTP', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts deleted file mode 100644 index 118cfa7e6f7..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/alerting.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as e2e from '@grafana/e2e-selectors'; -import { expect, test } from '@grafana/plugin-e2e'; - -// let's disable the feature toggles for now, otherwise the getAlertRuleQueryRow fails and I don't see any other way to get the query row -test.use({ featureToggles: { alertingQueryAndExpressionsStepMode: false, alertingNotificationsStepMode: false } }); -test('should evaluate to false if entire request returns 200 but partial query result is invalid', async ({ - page, - alertRuleEditPage, -}) => { - await alertRuleEditPage.alertRuleNameField.fill('Test Alert Rule'); - - //add working query - const queryA = alertRuleEditPage.getAlertRuleQueryRow('A'); - await queryA.datasource.set('gdev-prometheus'); - await queryA.locator.getByLabel('Code').click(); - await page.waitForFunction(() => window.monaco); - await queryA.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); - await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))'); - - //add broken query - const newQuery = await alertRuleEditPage.clickAddQueryRow(); - await newQuery.datasource.set('gdev-prometheus'); - await newQuery.locator.getByLabel('Code').click(); - await newQuery.getByGrafanaSelector(e2e.selectors.components.QueryField.container).click(); - await page.keyboard.insertText('topk(5,'); - - await expect(alertRuleEditPage.evaluate()).not.toBeOK(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts deleted file mode 100644 index b37ab2722bf..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.addPanel(); - await expect(page.url()).toContain('editPanel'); -}); - -test('add panel in new dashboard', async ({ dashboardPage, page }) => { - const panelEditPage = await dashboardPage.addPanel(); - await expect(panelEditPage.panel.locator).toBeVisible(); - await expect(page.url()).toContain('editPanel'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts deleted file mode 100644 index bf7ebe37653..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/datasourceConfigPage.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; - -test.describe('test createDataSourceConfigPage fixture, saveAndTest and toBeOK matcher', () => { - test('invalid credentials should return an error', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); - await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); - await expect( - configPage.saveAndTest(), - formatExpectError('Expected save data source config to fail when Prometheus server is not running') - ).not.toBeOK(); - }); - - test('valid credentials should return a 200 status code', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'prometheus' }); - configPage.mockHealthCheckResponse({ status: 200 }); - await page.getByPlaceholder('http://localhost:9090').fill('http://localhost:9090'); - await expect( - configPage.saveAndTest(), - formatExpectError('Expected data source config to be successfully saved') - ).toBeOK(); - }); -}); - -test.describe('test data source with frontend only health check', () => { - test('valid credentials should display a success alert on the page', async ({ createDataSourceConfigPage, page }) => { - const configPage = await createDataSourceConfigPage({ type: 'zipkin' }); - configPage.mockHealthCheckResponse({ message: 'Data source is working', status: 'OK' }, 200); - await page.getByPlaceholder('http://localhost:9411').fill('http://localhost:9411'); - await expect(configPage.saveAndTest()).toBeOK(); - await expect( - configPage, - formatExpectError('Expected data source config to display success alert after save') - ).toHaveAlert('success', { hasNotText: 'Datasource updated' }); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts deleted file mode 100644 index a4823228495..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/explorePage.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; - -test('query data response should be OK when query is valid', async ({ explorePage }) => { - await explorePage.datasource.set('gdev-testdata'); - await expect(explorePage.runQuery(), formatExpectError('Expected Explore query to execute successfully')).toBeOK(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts deleted file mode 100644 index 48ded217e40..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/featureToggles.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -const TRUTHY_CUSTOM_TOGGLE = 'custom_toggle1'; -const FALSY_CUSTOM_TOGGLE = 'custom_toggle2'; - -// override the feature toggles defined in playwright.config.ts only for tests in this file -test.use({ - featureToggles: { - [TRUTHY_CUSTOM_TOGGLE]: true, - [FALSY_CUSTOM_TOGGLE]: false, - }, -}); - -test('should set and check feature toggles correctly', async ({ isFeatureToggleEnabled }) => { - expect(await isFeatureToggleEnabled(TRUTHY_CUSTOM_TOGGLE)).toBeTruthy(); - expect(await isFeatureToggleEnabled(FALSY_CUSTOM_TOGGLE)).toBeFalsy(); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts deleted file mode 100644 index c55cc76b42c..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/lokiEditor.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as e2e from '@grafana/e2e-selectors'; -import { expect, test } from '@grafana/plugin-e2e'; - -test.describe('Loki editor', () => { - test('Autocomplete features should work as expected.', async ({ page }) => { - // Go to loki datasource in explore - await page.goto( - '/explore?schemaVersion=1&panes=%7B%22iap%22:%7B%22datasource%22:%22gdev-loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22gdev-loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&orgId=1' - ); - - const queryEditor = page.getByTestId(e2e.selectors.components.QueryField.container); - const queryEditorRows = page.getByTestId('query-editor-rows'); - - async function assertQueryEditorEmpty() { - const queryEditorEmptyText = /^Enter to Rename.+/; - await expect(queryEditor).toHaveText(queryEditorEmptyText); - } - - async function clearInput() { - // Clear focused input - // Monaco appears to need some time to init keybindings after a change, adding this timeout to prevent flake - await page.waitForTimeout(100); - await page.keyboard.press('ControlOrMeta+A'); - await page.keyboard.press('Backspace'); - } - - // assert that the query builder is shown by default - await expect(page.getByText('Label filters')).toHaveCount(1); - - // switch to code editor - await page.getByLabel('Code').click(); - - await page.waitForFunction(() => window.monaco); - await expect(queryEditor).toHaveCount(1); - await assertQueryEditorEmpty(); - - // assert editor automatically adds close paren - await queryEditor.click(); - await page.keyboard.type('time('); - await expect(queryEditor).toContainText('time()'); - - // removes closing brace when opening brace is removed - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('avg_over_time('); - await expect(queryEditor).toContainText('avg_over_time()'); - await page.keyboard.press('Backspace'); - await expect(queryEditor).not.toContainText('avg_over_time()'); - await expect(queryEditor).toContainText('avg_over_time'); - - // keeps closing brace when opening brace is removed and inner values exist - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('time(test'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('ArrowLeft'); - await page.keyboard.press('Backspace'); - await expect(queryEditor).toContainText('timetest'); - - // overrides an automatically inserted paren - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('time()'); - await expect(queryEditor).toContainText('time()'); - - // does not override manually inserted braces - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('))'); - await expect(queryEditor).toContainText('))'); - - // Should execute the query when enter with shift is pressed - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.press('Shift+Enter'); - await expect(page.getByTestId('explore-no-data')).toHaveCount(1); - - // Suggestions plugin - await clearInput(); - await assertQueryEditorEmpty(); - await page.keyboard.type('av'); - await expect(queryEditorRows.getByLabel(/avg, docs:/)).toHaveCount(1); - await expect(queryEditorRows.getByLabel(/avg_over_time, docs:/)).toHaveCount(1); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts deleted file mode 100644 index 29bd1b834dc..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { successfulDataQuery } from '../mocks/queries'; - -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test.describe('panel edit page', () => { - test('table panel data assertions with provisioned dashboard', async ({ gotoPanelEditPage }) => { - const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['Field', 'Max', 'Mean', 'Last']); - }); - - test('table panel data assertions', async ({ panelEditPage }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Table'); - await panelEditPage.refreshPanel(); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['col1', 'col2']); - await expect(panelEditPage.panel.data, formatExpectError('Could not locate headers in table panel')).toContainText([ - 'val1', - 'val2', - 'val3', - 'val4', - ]); - }); - - test('timeseries panel - table view assertions', async ({ panelEditPage }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Time series'); - await panelEditPage.refreshPanel(); - await panelEditPage.toggleTableView(); - await expect( - panelEditPage.panel.locator, - formatExpectError('Could not locate panel in panel edit page') - ).toBeVisible(); - await expect( - panelEditPage.panel.fieldNames, - formatExpectError('Could not locate header elements in table panel') - ).toContainText(['col1', 'col2']); - await expect( - panelEditPage.panel.data, - formatExpectError('Could not locate data elements in table panel') - ).toContainText(['val1', 'val2', 'val3', 'val4']); - }); -}); - -test.describe('dashboard page', () => { - test('getting panel by title', async ({ gotoDashboardPage }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.goto(); - const panel = await dashboardPage.getPanelByTitle('Colored background'); - await expect(panel.fieldNames).toContainText(['Field', 'Max', 'Mean', 'Last']); - }); - - test('getting panel by id', async ({ gotoDashboardPage }) => { - const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); - await dashboardPage.goto(); - const panel = await dashboardPage.getPanelByTitle('Colored background'); - await expect(panel.fieldNames, formatExpectError('Could not locate header elements in table panel')).toContainText([ - 'Field', - 'Max', - 'Mean', - 'Last', - ]); - }); -}); - -test.describe('explore page', () => { - test('table panel', async ({ explorePage }) => { - const url = - 'left=%7B"datasource":"grafana","queries":%5B%7B"queryType":"randomWalk","refId":"A","datasource":%7B"type":"datasource","uid":"grafana"%7D%7D%5D,"range":%7B"from":"1547161200000","to":"1576364400000"%7D%7D&orgId=1'; - await explorePage.goto({ - queryParams: new URLSearchParams(url), - }); - await expect( - explorePage.timeSeriesPanel.locator, - formatExpectError('Could not locate time series panel in explore page') - ).toBeVisible(); - await expect( - explorePage.tablePanel.locator, - formatExpectError('Could not locate table panel in explore page') - ).toBeVisible(); - await expect(explorePage.tablePanel.fieldNames).toContainText(['time', 'A-series']); - }); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts deleted file mode 100644 index f8bc623f267..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { successfulDataQuery } from '../mocks/queries'; -import { scenarios } from '../mocks/resources'; - -const PANEL_TITLE = 'Table panel E2E test'; -const TABLE_VIZ_NAME = 'Table'; -const TIME_SERIES_VIZ_NAME = 'Time series'; -const STANDARD_OTIONS_CATEGORY = 'Standard options'; -const DISPLAY_NAME_LABEL = 'Display name'; -const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; - -test.describe('query editor query data', () => { - test('query data response should be OK when query is valid', async ({ panelEditPage }) => { - await panelEditPage.datasource.set('gdev-testdata'); - await expect( - panelEditPage.refreshPanel(), - formatExpectError('Expected panel query to execute successfully') - ).toBeOK(); - }); - - test('query data response should not be OK and panel error should be displayed when query is invalid', async ({ - panelEditPage, - }) => { - await panelEditPage.datasource.set('gdev-testdata'); - const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); - await queryEditorRow.getByLabel('Labels').fill('invalid-label-format'); - await expect(panelEditPage.refreshPanel(), formatExpectError('Expected panel query to fail')).not.toBeOK(); - await expect( - panelEditPage.panel.getErrorIcon(), - formatExpectError('Expected panel error to be displayed after query execution') - ).toBeVisible(); - }); -}); - -test.describe('query editor with mocked responses', () => { - test('and resource `scenarios` is mocked', async ({ selectors, dashboardPage }) => { - await dashboardPage.mockResourceResponse('scenarios', scenarios); - const panelEditPage = await dashboardPage.addPanel(); - await panelEditPage.datasource.set('gdev-testdata'); - const queryEditorRow = await panelEditPage.getQueryEditorRow('A'); - await queryEditorRow.getByLabel('Scenario').last().click(); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Select.option), - formatExpectError('Expected certain select options to be displayed after clicking on the select input') - ).toHaveText(scenarios.map((s) => s.name)); - }); - - test('mocked query data response', async ({ panelEditPage, selectors }) => { - await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); - await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization(TABLE_VIZ_NAME); - await panelEditPage.refreshPanel(); - await expect( - panelEditPage.panel.getErrorIcon(), - formatExpectError('Did not expect panel error to be displayed after query execution') - ).not.toBeVisible(); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Table.body), - formatExpectError('Expected certain select options to be displayed after clicking on the select input') - ).toHaveText('val1val2val3val4'); - }); -}); - -test.describe('edit panel plugin settings', () => { - test('change viz to table panel, set panel title and collapse section', async ({ - panelEditPage, - selectors, - page, - }) => { - await panelEditPage.setVisualization(TABLE_VIZ_NAME); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker), - formatExpectError('Expected panel visualization to be set to table') - ).toHaveText(TABLE_VIZ_NAME); - await panelEditPage.setPanelTitle(PANEL_TITLE); - await expect( - panelEditPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(PANEL_TITLE)), - formatExpectError('Expected panel title to be updated') - ).toBeVisible(); - await panelEditPage.collapseSection(STANDARD_OTIONS_CATEGORY); - await expect( - page.getByText(DISPLAY_NAME_LABEL), - formatExpectError('Expected section to be collapsed') - ).toBeVisible(); - }); - - test('Select time zone in timezone picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = await panelEditPage.getCustomOptions('Axis'); - const timeZonePicker = axisOptions.getSelect('Time zone'); - - await timeZonePicker.selectOption('Europe/Stockholm'); - await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); - }); - - test('select unit in unit picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const standardOptions = panelEditPage.getStandardOptions(); - const unitPicker = standardOptions.getUnitPicker('Unit'); - - await unitPicker.selectOption('Misc > Pixels'); - - await expect(unitPicker).toHaveSelected('Pixels'); - }); - - test('enter value in number input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const lineWith = axisOptions.getNumberInput('Soft min'); - - await lineWith.fill('10'); - - await expect(lineWith).toHaveValue('10'); - }); - - test('enter value in slider', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const graphOptions = panelEditPage.getCustomOptions('Graph styles'); - const lineWidth = graphOptions.getSliderInput('Line width'); - - await lineWidth.fill('10'); - - await expect(lineWidth).toHaveValue('10'); - }); - - test('select value in single value select', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const standardOptions = panelEditPage.getStandardOptions(); - const colorSchemeSelect = standardOptions.getSelect('Color scheme'); - - await colorSchemeSelect.selectOption('Classic palette'); - await expect(colorSchemeSelect).toHaveSelected('Classic palette'); - }); - - test('clear input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const panelOptions = panelEditPage.getPanelOptions(); - const title = panelOptions.getTextInput('Title'); - - await expect(title).toHaveValue('New panel'); - await title.clear(); - await expect(title).toHaveValue(''); - }); - - test('enter value in input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const panelOptions = panelEditPage.getPanelOptions(); - const description = panelOptions.getTextInput('Description'); - - await expect(description).toHaveValue(''); - await description.fill('This is a panel'); - await expect(description).toHaveValue('This is a panel'); - }); - - test('unchecking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const showBorder = axisOptions.getSwitch('Show border'); - - await expect(showBorder).toBeChecked({ checked: false }); - await showBorder.check(); - await expect(showBorder).toBeChecked(); - - await showBorder.uncheck(); - await expect(showBorder).toBeChecked({ checked: false }); - }); - - test('checking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const showBorder = axisOptions.getSwitch('Show border'); - - await expect(showBorder).toBeChecked({ checked: false }); - await showBorder.check(); - await expect(showBorder).toBeChecked(); - }); - - test('re-selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const placement = axisOptions.getRadioGroup('Placement'); - - await placement.check('Right'); - await expect(placement).toHaveChecked('Right'); - - await placement.check('Auto'); - await expect(placement).toHaveChecked('Auto'); - }); - - test('selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); - const axisOptions = panelEditPage.getCustomOptions('Axis'); - const placement = axisOptions.getRadioGroup('Placement'); - - await placement.check('Right'); - await expect(placement).toHaveChecked('Right'); - }); -}); - -test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { - const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); - await panelEditPage.backToDashboard(); - await expect(page.url()).not.toContain('editPanel'); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts deleted file mode 100644 index b93d9a8e5dd..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -import { formatExpectError } from '../errors'; -import { prometheusLabels } from '../mocks/resources'; - -test('variable query with mocked response', async ({ variableEditPage, page }) => { - variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); - variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); - await variableEditPage.datasource.set('gdev-prometheus'); - await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); - await page.keyboard.press('Tab'); - await variableEditPage.runQuery(); - await expect( - variableEditPage, - formatExpectError('Expected variable edit page to display certain label names after query execution') - ).toDisplayPreviews(prometheusLabels.data); -}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts deleted file mode 100644 index 7c7711fa625..00000000000 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { expect, test } from '@grafana/plugin-e2e'; - -test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { - await page.goto('/'); - const homePageURL = new URL(page.url()); - await page.goto('/datasources', { waitUntil: 'networkidle' }); - const redirectedPageURL = new URL(page.url()); - expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); -}); - -test('current user should have viewer role', async ({ page, request }) => { - await page.goto('/'); - const response = await request.get('/api/user/orgs'); - await expect(response).toBeOK(); - await expect(await response.json()).toContainEqual(expect.objectContaining({ role: 'Viewer' })); -}); diff --git a/e2e/plugin-e2e/postgres/postgres.spec.ts b/e2e/plugin-e2e/postgres/postgres.spec.ts deleted file mode 100644 index aafdcbee40b..00000000000 --- a/e2e/plugin-e2e/postgres/postgres.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'grafana-postgresql-datasource' }); - - await expect(await page.getByText('Type: PostgreSQL', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/plugin-e2e/zipkin/zipkin.spec.ts b/e2e/plugin-e2e/zipkin/zipkin.spec.ts deleted file mode 100644 index 5a58dc656fb..00000000000 --- a/e2e/plugin-e2e/zipkin/zipkin.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { - await createDataSourceConfigPage({ type: 'zipkin' }); - - await expect(await page.getByText('Type: Zipkin', { exact: true })).toBeVisible(); - await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts deleted file mode 100644 index 12d270ae565..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/useExposedComponent.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; -import { testIds } from '../testIds'; -import pluginJson from '../plugin.json'; - -test('should display component exposed by another app', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/exposed-components`); - await expect(page.getByTestId(testIds.appB.exposedComponent)).toHaveText('Hello World!'); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts deleted file mode 100644 index a0d6d2246a0..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginComponents.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -import pluginJson from '../plugin.json'; -import { testIds } from '../testIds'; - -test('should render component with usePluginComponents hook', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-components`); - await expect( - page.getByTestId(testIds.addedComponentsPage.container).getByTestId(testIds.appB.reusableAddedComponent) - ).toHaveText('Hello World!'); -}); diff --git a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts b/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts deleted file mode 100644 index 008d3ec5baf..00000000000 --- a/e2e/test-plugins/grafana-extensionstest-app/tests/usePluginLinks.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -import pluginJson from '../plugin.json'; -import testApp3pluginJson from '../plugins/grafana-extensionexample3-app/plugin.json'; -import { testIds } from '../testIds'; - -test('should extend the actions menu with a link to a-app plugin', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Go to A').click(); - await page.getByTestId(testIds.modal.open).click(); - await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); -}); - -test('should extend main app with link extension from app B', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Open from B').click(); - await expect(page.getByTestId(testIds.appB.modal)).toBeVisible(); -}); - -test('should extend main app with basic link extension from app A', async ({ page }) => { - await page.goto(`/a/${pluginJson.id}/added-links`); - const section = await page.getByTestId(testIds.addedLinksPage.section1); - await section.getByTestId(testIds.actions.button).click(); - await page.getByTestId(testIds.container).getByText('Basic link').click(); - await page.getByTestId(testIds.modal.open).click(); - await expect(page.getByTestId(testIds.appA.container)).toBeVisible(); -}); - -test('should not display any extensions when extension point is not declared in plugin json when in development mode', async ({ - page, -}) => { - await page.goto(`/a/${testApp3pluginJson.id}`); - const container = await page.getByTestId(testIds.appC.section1); - await expect(container.getByTestId(testIds.actions.button)).not.toBeVisible(); -}); - -const panelTitle = 'Link with defaults'; -const extensionTitle = 'Open from time series...'; - -const linkOnClickDashboardUid = 'dbfb47c5-e5e5-4d28-8ac7-35f349b95946'; -const linkPathDashboardUid = 'd1fbb077-cd44-4738-8c8a-d4e66748b719'; - -test('configureExtensionLink - should add link extension (path) with defaults to time series panel.', async ({ - gotoDashboardPage, - page, -}) => { - const dashboardPage = await gotoDashboardPage({ uid: linkPathDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('heading', { name: 'Extensions test app' })).toBeVisible(); -}); - -test('should add link extension (onclick) with defaults to time series panel', async ({ gotoDashboardPage, page }) => { - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with defaults"'); -}); - -test('should add link extension (onclick) with new title to pie chart panel', async ({ gotoDashboardPage, page }) => { - const panelTitle = 'Link with new name'; - const extensionTitle = 'Open from piechart'; - const dashboardPage = await gotoDashboardPage({ uid: linkOnClickDashboardUid }); - const panel = await dashboardPage.getPanelByTitle(panelTitle); - await panel.clickOnMenuItem(extensionTitle, { parentItem: 'Extensions' }); - await expect(page.getByRole('dialog')).toContainText('Select query from "Link with new name"'); -}); diff --git a/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts b/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts deleted file mode 100644 index b72ff0580b5..00000000000 --- a/e2e/test-plugins/grafana-test-datasource/tests/configEditor.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { test, expect, DataSourceConfigPage } from '@grafana/plugin-e2e'; - -// The following tests verify that label and input field association is working correctly. -// If these tests break, e2e tests in external plugins will break too. - -test.describe('config editor ', () => { - let configPage: DataSourceConfigPage; - test.beforeEach(async ({ createDataSourceConfigPage }) => { - configPage = await createDataSourceConfigPage({ type: 'grafana-e2etest-datasource' }); - }); - - test('text input field', async ({ page }) => { - const field = page.getByRole('textbox', { name: 'API key' }); - await expect(field).toBeEmpty(); - await field.fill('test text'); - await expect(field).toHaveValue('test text'); - }); - - test('switch field', async ({ page }) => { - const field = page.getByLabel('Switch Enabled'); - await expect(field).not.toBeChecked(); - await field.check(); - await expect(field).toBeChecked(); - }); - - test('checkbox field', async ({ page }) => { - const field = page.getByRole('checkbox', { name: 'Checkbox Enabled' }); - await expect(field).not.toBeChecked(); - await field.check({ force: true }); - await expect(field).toBeChecked(); - }); - - test('select field', async ({ page, selectors }) => { - const field = page.getByRole('combobox', { name: 'Auth type' }); - await field.click(); - const option = selectors.components.Select.option; - await expect(configPage.getByGrafanaSelector(option)).toHaveText(['keys', 'credentials']); - }); -}); diff --git a/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts b/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts deleted file mode 100644 index 72565e5a885..00000000000 --- a/e2e/test-plugins/grafana-test-datasource/tests/variables.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test, expect } from '@grafana/plugin-e2e'; - -test('should render variable editor', async ({ variableEditPage, page }) => { - await variableEditPage.datasource.set('gdev-e2etestdatasource'); - await expect(page.getByRole('textbox', { name: 'Query Text' })).toBeVisible(); -}); - -test('create new, successful variable query', async ({ variableEditPage, page }) => { - await variableEditPage.datasource.set('gdev-e2etestdatasource'); - await page.getByRole('textbox', { name: 'Query Text' }).fill('variableQuery'); - await variableEditPage.runQuery(); - await expect(variableEditPage).toDisplayPreviews(['A', 'B']); -}); diff --git a/eslint.config.js b/eslint.config.js index 9f5bcc9da52..d5bd122f12c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -43,7 +43,7 @@ module.exports = [ 'data/', 'deployment_tools_config.json', 'devenv', - 'e2e/test-plugins', + 'e2e-playwright/test-plugins', 'e2e/tmp', 'packages/grafana-ui/src/components/Icon/iconBundle.ts', 'pkg', diff --git a/package.json b/package.json index 617104b7a1f..55d58e1198c 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", "e2e:playwright": "yarn playwright test", - "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", + "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e-playwright/start-and-run-suite", + "e2e:playwright:storybook": "yarn playwright test -c playwright.storybook.config.ts", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", "e2e:plugin:build": "nx run-many -t build --projects='@test-plugins/*'", "e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100", @@ -454,7 +455,7 @@ "packages": [ "packages/*", "public/app/plugins/*/*", - "e2e/test-plugins/*" + "e2e-playwright/test-plugins/*" ] }, "engines": { diff --git a/pkg/build/a11y/main.go b/pkg/build/a11y/main.go index 444e59213d2..dd85c2c3328 100644 --- a/pkg/build/a11y/main.go +++ b/pkg/build/a11y/main.go @@ -91,7 +91,7 @@ func run(ctx context.Context, cmd *cli.Command) error { hostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ Include: []string{ "./devenv", - "./e2e/test-plugins", // Directory is included so provisioning works, but they're not actually build + "./e2e-playwright/test-plugins", // Directory is included so provisioning works, but they're not actually build "./scripts/grafana-server/custom.ini", "./scripts/grafana-server/start-server", "./scripts/grafana-server/kill-server", diff --git a/pkg/build/a11y/service.go b/pkg/build/a11y/service.go index c866bc60520..98bcb5cf20b 100644 --- a/pkg/build/a11y/service.go +++ b/pkg/build/a11y/service.go @@ -22,7 +22,7 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithExec([]string{"mkdir", "-p", "/src/grafana"}). WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")). - WithDirectory("/src/grafana/e2e/test-plugins", opts.HostSrc.Directory("./e2e/test-plugins")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", opts.HostSrc.Directory("./e2e-playwright/test-plugins")). WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")). WithWorkdir("/src/grafana"). WithEnvVariable("GF_APP_MODE", "development"). diff --git a/pkg/build/e2e-playwright/README.md b/pkg/build/e2e-playwright/README.md new file mode 100644 index 00000000000..d5fc311d696 --- /dev/null +++ b/pkg/build/e2e-playwright/README.md @@ -0,0 +1,22 @@ +# Pa11y accessability tests + +We use pa11y to run some automated simple accessability tests. They're ran with dagger to help orchestrate starting server + tests in a reproducable manner. + +To run the tests locally: + +1. Install dagger locally https://docs.dagger.io/install/ +2. Grab the grafana.tar.gz artifact by either + 1. Downloading it from the Github Action artifact from your PR + 1. Build it locally with: + ```sh + dagger run go run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="$PWD" > dist/files.txt + cat dist/files.txt # Will output the path to the grafana.tar.gz + ``` +3. Run the dagger pipeline with: + ```sh + dagger -v run go run ./pkg/build/a11y --package=(full path to .tar.gz) --results=./pa11y-ci-results.json + ``` + The JSON results file will be saved to the file from the `--results` arg +4. If they fail and you want to see the full output + 1. Run the dagger command with `dagger -vE [...]` + 2. At the end, arrow up to the exec pa11y-ci segment and hit Enter \ No newline at end of file diff --git a/pkg/build/e2e-playwright/e2e.go b/pkg/build/e2e-playwright/e2e.go new file mode 100644 index 00000000000..35579e8dd2f --- /dev/null +++ b/pkg/build/e2e-playwright/e2e.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "dagger.io/dagger" +) + +var ( + // Locations in the container to write results to + testResultsDir = "/playwright-test-results" + htmlResultsDir = "/playwright-html-report" + blobResultsDir = "/playwright-blob-report" +) + +type RunTestOpts struct { + GrafanaService *dagger.Service + FrontendContainer *dagger.Container + HostSrc *dagger.Directory + Shard string + HTMLReportExportDir string + BlobReportExportDir string + TestResultsExportDir string +} + +func RunTest( + ctx context.Context, + d *dagger.Client, + opts RunTestOpts, +) (*dagger.Container, error) { + playwrightCommand := buildPlaywrightCommand(opts) + + e2eContainer := opts.FrontendContainer. + WithWorkdir("/src"). + WithDirectory("/src", opts.HostSrc). + WithMountedCache(".nx", d.CacheVolume("nx-cache")). + WithEnvVariable("HOST", grafanaHost). + WithEnvVariable("PORT", fmt.Sprint(grafanaPort)). + WithServiceBinding(grafanaHost, opts.GrafanaService). + WithEnvVariable("bustcache", "1"). + WithEnvVariable("PLAYWRIGHT_HTML_OPEN", "never"). + WithEnvVariable("PLAYWRIGHT_HTML_OUTPUT_DIR", htmlResultsDir). + WithEnvVariable("PLAYWRIGHT_BLOB_OUTPUT_DIR", blobResultsDir). + WithExec(playwrightCommand, dagger.ContainerWithExecOpts{ + Expect: dagger.ReturnTypeAny, + }) + + if opts.TestResultsExportDir != "" { + _, err := e2eContainer.Directory(testResultsDir).Export(ctx, opts.TestResultsExportDir) + if err != nil { + return nil, err + } + } + + if opts.HTMLReportExportDir != "" { + _, err := e2eContainer.Directory(htmlResultsDir).Export(ctx, opts.HTMLReportExportDir) + if err != nil { + return nil, err + } + } + + if opts.BlobReportExportDir != "" { + _, err := e2eContainer.Directory(blobResultsDir).Export(ctx, opts.BlobReportExportDir) + if err != nil { + return nil, err + } + } + + return e2eContainer, nil +} + +func buildPlaywrightCommand(opts RunTestOpts) []string { + playwrightReporters := []string{ + "dot", // minimal output in shards + } + + if opts.HTMLReportExportDir != "" { + playwrightReporters = append(playwrightReporters, "html") + } + + if opts.BlobReportExportDir != "" { + playwrightReporters = append(playwrightReporters, "blob") + } + + playwrightCommand := []string{ + "yarn", + "e2e:playwright", + "--reporter", + strings.Join(playwrightReporters, ","), + "--output", + testResultsDir, + } + + if opts.Shard != "" { + playwrightCommand = append(playwrightCommand, "--shard", opts.Shard) + } + + return playwrightCommand +} diff --git a/pkg/build/e2e-playwright/frontend.go b/pkg/build/e2e-playwright/frontend.go new file mode 100644 index 00000000000..625ea155ae4 --- /dev/null +++ b/pkg/build/e2e-playwright/frontend.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "dagger.io/dagger" +) + +type Deps struct { + NodeVersion string + PlaywrightVersion string +} + +// Create a container with frontend dependencies installed, ready to build plugins +// or run e2e tests. +// Theoretically we would setup Playwright in e2e.go, but to optimise layer caching +// we want it to happen before yarn install. +func WithFrontendContainer(ctx context.Context, d *dagger.Client, yarnHostSrc *dagger.Directory) (*dagger.Container, error) { + deps, err := GetVersions(ctx, yarnHostSrc) + if err != nil { + return nil, err + } + + nodeBase := WithNode(d, deps.NodeVersion) + playwrightBase := WithPlaywright(d, nodeBase, deps.PlaywrightVersion) + + return WithYarnInstall(d, playwrightBase, yarnHostSrc), nil +} + +func GetVersions(ctx context.Context, src *dagger.Directory) (Deps, error) { + nvmrc, err := src.File(".nvmrc").Contents(ctx) + if err != nil { + return Deps{}, err + } + pkgJSON, err := src.File("package.json").Contents(ctx) + if err != nil { + return Deps{}, err + } + + // parse package.json + var pkgJson struct { + DevDependencies map[string]string `json:"devDependencies"` + } + if err := json.Unmarshal([]byte(pkgJSON), &pkgJson); err != nil { + return Deps{}, err + } + + return Deps{ + NodeVersion: strings.TrimSpace(strings.TrimPrefix(nvmrc, "v")), + PlaywrightVersion: strings.TrimSpace(pkgJson.DevDependencies["@playwright/test"]), + }, nil +} + +func WithNode(d *dagger.Client, version string) *dagger.Container { + nodeImage := fmt.Sprintf("node:%s-slim", strings.TrimPrefix(version, "v")) + return d.Container().From(nodeImage) +} + +func WithPlaywright(d *dagger.Client, base *dagger.Container, version string) *dagger.Container { + brCache := d.CacheVolume("playwright-browsers") + return base. + WithEnvVariable("PLAYWRIGHT_BROWSERS_PATH", "/playwright-cache"). + WithMountedCache("/playwright-cache", brCache). + WithExec([]string{"npx", "-y", "playwright@" + version, "install", "--with-deps"}) +} + +func WithYarnInstall(d *dagger.Client, base *dagger.Container, yarnHostSrc *dagger.Directory) *dagger.Container { + yarnCache := d.CacheVolume("yarn-cache") + + return base. + WithWorkdir("/src"). + WithMountedCache("/.yarn", yarnCache). + WithEnvVariable("YARN_CACHE_FOLDER", "/.yarn"). + WithEnvVariable("CYPRESS_INSTALL_BINARY", "0"). // Don't download Cypress binaries + + // It's important to copy all files here because the whole src directory is then copied into the test runner container + WithDirectory("/src", yarnHostSrc). + WithExec([]string{"corepack", "enable"}). + WithExec([]string{"corepack", "install"}). + WithExec([]string{"yarn", "install", "--immutable"}) +} diff --git a/pkg/build/e2e-playwright/main.go b/pkg/build/e2e-playwright/main.go new file mode 100644 index 00000000000..09384c8614b --- /dev/null +++ b/pkg/build/e2e-playwright/main.go @@ -0,0 +1,243 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + + "dagger.io/dagger" + "github.com/urfave/cli/v3" +) + +var ( + grafanaHost = "grafana" + grafanaPort = 3001 +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := NewApp().Run(ctx, os.Args); err != nil { + cancel() + fmt.Println(err) + os.Exit(1) + } +} + +func NewApp() *cli.Command { + return &cli.Command{ + Name: "a11y", + Usage: "Run Grafana playwright e2e tests", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "grafana-dir", + Usage: "Path to the grafana/grafana clone directory", + Value: ".", + Validator: mustBeDir("grafana-dir", false, false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "package", + Usage: "Path to the grafana tar.gz package", + Value: "grafana.tar.gz", + Validator: mustBeFile("package", false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "license", + Usage: "Path to the Grafana Enterprise license file (optional)", + Validator: mustBeFile("license", true), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "shard", + Usage: "Test shard to run. See Playwright docs", + }, + &cli.StringFlag{ + Name: "results-dir", + Usage: "Path to the directory to export the playwright test results to (optional)", + Validator: mustBeDir("results-dir", true, true), + }, + &cli.StringFlag{ + Name: "html-dir", + Usage: "Enables the HTML reporter, exported to this directory (optional)", + Validator: mustBeDir("html-dir", true, true), + }, + &cli.StringFlag{ + Name: "blob-dir", + Usage: "Enables the blob reporter, exported to this directory. Useful with --shard (optional)", + Validator: mustBeDir("blob-dir", true, true), + }, + }, + Action: run, + } +} + +func run(ctx context.Context, cmd *cli.Command) error { + grafanaDir := cmd.String("grafana-dir") + targzPath := cmd.String("package") + licensePath := cmd.String("license") + pwShard := cmd.String("shard") + resultsDir := cmd.String("results-dir") + htmlDir := cmd.String("html-dir") + blobDir := cmd.String("blob-dir") + // pa11yConfigPath := cmd.String("config") + // pa11yResultsPath := cmd.String("results") + // noThresholdFail := cmd.Bool("no-threshold-fail") + + d, err := dagger.Connect(ctx) + if err != nil { + return fmt.Errorf("failed to connect to Dagger: %w", err) + } + + // Explicitly only the files used by the grafana-server service + grafanaHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ + Include: []string{ + "./devenv", + + // Must build test plugins to run e2e tests + "./e2e-playwright/test-plugins", + "./packages/grafana-plugin-configs", + + "./scripts/grafana-server/custom.ini", + "./scripts/grafana-server/start-server", + "./scripts/grafana-server/kill-server", + "./scripts/grafana-server/variables", + }, + }) + + // Minimal files needed to run yarn install + yarnHostSrc := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ + Include: []string{ + "package.json", + "yarn.lock", + ".yarnrc.yml", + ".yarn", + "packages/*/package.json", + "packages/grafana-plugin-configs", + "public/app/plugins/*/*/package.json", + "e2e-playwright/test-plugins/*/package.json", + ".nvmrc", + }, + }) + + // Files needed to run e2e tests. yarnHostSrc will be copied into the test runner container as well. + e2eHostSrc := d.Host().Directory(".", dagger.HostDirectoryOpts{ + Include: []string{ + "public/app/types/*.d.ts", + "public/app/core/icons/cached.json", + + // packages we use in playwright tests + "packages", // TODO: do we need all of this? + "public/app/plugins", // TODO: do we need all of this? + + // e2e files + "e2e-playwright", + "e2e-playwright/test-plugins", + "playwright.config.ts", + }, + Exclude: []string{ + "**/dist", + }, + }) + + frontendContainer, err := WithFrontendContainer(ctx, d, yarnHostSrc) + if err != nil { + return fmt.Errorf("failed to create frontend container: %w", err) + } + + targz := d.Host().File(targzPath) + + var license *dagger.File + if licensePath != "" { + license = d.Host().File(licensePath) + } + + svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{ + HostSrc: grafanaHostSrc, + FrontendContainer: frontendContainer, + GrafanaTarGz: targz, + License: license, + }) + if err != nil { + return fmt.Errorf("failed to create Grafana service: %w", err) + } + + runOpts := RunTestOpts{ + GrafanaService: svc, + FrontendContainer: frontendContainer, + HostSrc: e2eHostSrc, + Shard: pwShard, + TestResultsExportDir: resultsDir, + HTMLReportExportDir: htmlDir, + BlobReportExportDir: blobDir, + } + + c, runErr := RunTest(ctx, d, runOpts) + if runErr != nil { + return fmt.Errorf("failed to run e2e test suite: %w", runErr) + } + + c, syncErr := c.Sync(ctx) + if syncErr != nil { + return fmt.Errorf("failed to sync e2e test suite: %w", syncErr) + } + + code, codeErr := c.ExitCode(ctx) + if codeErr != nil { + return fmt.Errorf("failed to get exit code of e2e test suite: %w", codeErr) + } + + if code == 0 { + log.Printf("e2e tests passed with exit code %d", code) + } else { + return fmt.Errorf("e2e tests failed with exit code %d", code) + } + + log.Println("e2e tests completed successfully") + return nil +} + +func mustBeFile(arg string, emptyOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if stat.IsDir() { + return cli.Exit(arg+" must be a file, not a directory: "+s, 1) + } + return nil + } +} + +func mustBeDir(arg string, emptyOk bool, notExistOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + if notExistOk { + return nil + } + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if !stat.IsDir() { + return cli.Exit(arg+" must be a directory: "+s, 1) + } + return nil + } +} diff --git a/pkg/build/e2e-playwright/service.go b/pkg/build/e2e-playwright/service.go new file mode 100644 index 00000000000..d162451ef70 --- /dev/null +++ b/pkg/build/e2e-playwright/service.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "dagger.io/dagger" +) + +type GrafanaServiceOpts struct { + HostSrc *dagger.Directory + FrontendContainer *dagger.Container + GrafanaTarGz *dagger.File + License *dagger.File +} + +func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOpts) (*dagger.Service, error) { + testPlugins := opts.FrontendContainer. + WithDirectory("e2e-playwright/test-plugins", opts.HostSrc.Directory("./e2e-playwright/test-plugins")). + WithDirectory("packages/grafana-plugin-configs", opts.HostSrc.Directory("./packages/grafana-plugin-configs")). + WithExec([]string{"yarn", "e2e:plugin:build"}) + + container := d.Container().From("alpine:3"). + WithExec([]string{"apk", "add", "--no-cache", "bash", "tar", "netcat-openbsd", "util-linux"}). + WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz). + WithExec([]string{"mkdir", "-p", "/src/grafana"}). + WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). + WithDirectory("/src/grafana/devenv", opts.HostSrc.Directory("./devenv")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", testPlugins.Directory("./e2e-playwright/test-plugins")). + WithDirectory("/src/grafana/scripts", opts.HostSrc.Directory("./scripts")). + WithWorkdir("/src/grafana"). + // Only set config variables here that are specific to the dagger/GHA runner. + // Prefer to configure scripts/grafana-server/custom.ini instead so they're also used + // when running locally. + WithEnvVariable("GF_SERVER_HTTP_PORT", fmt.Sprint(grafanaPort)). + WithExposedPort(grafanaPort) + + var licenseArg string + if opts.License != nil { + licenseArg = "/src/license.jwt" + container = container.WithMountedFile(licenseArg, opts.License) + } + + // We add all GF_ environment variables to allow for overriding Grafana configuration. + // It is unlikely the runner has any such otherwise. + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GF_") { + parts := strings.SplitN(env, "=", 2) + container = container.WithEnvVariable(parts[0], parts[1]) + } + } + + svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "scripts/grafana-server/start-server", licenseArg}}) + + return svc, nil +} diff --git a/pkg/build/e2e/service.go b/pkg/build/e2e/service.go index 95de093ec18..5c6eb0c4e78 100644 --- a/pkg/build/e2e/service.go +++ b/pkg/build/e2e/service.go @@ -82,6 +82,7 @@ func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOp WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). WithDirectory("/src/grafana/devenv", src.Directory("/src/devenv")). WithDirectory("/src/grafana/e2e", src.Directory("/src/e2e")). + WithDirectory("/src/grafana/e2e-playwright/test-plugins", src.Directory("/src/e2e-playwright/test-plugins")). WithDirectory("/src/grafana/scripts", src.Directory("/src/scripts")). WithDirectory("/src/grafana/tools", src.Directory("/src/tools")). WithWorkdir("/src/grafana"). diff --git a/playwright.config.ts b/playwright.config.ts index 39ac6aebd5f..648e464f387 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,7 +3,8 @@ import path, { dirname } from 'path'; import { PluginOptions } from '@grafana/plugin-e2e'; -const testDirRoot = 'e2e/plugin-e2e/'; +const testDirRoot = 'e2e-playwright'; +const pluginDirRoot = path.join(testDirRoot, 'plugin-e2e'); export default defineConfig({ fullyParallel: true, @@ -11,7 +12,9 @@ export default defineConfig({ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, - reporter: 'html', + reporter: [ + ['html'], // pretty + ], use: { baseURL: `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`, trace: 'retain-on-failure', @@ -19,6 +22,8 @@ export default defineConfig({ username: 'admin', password: 'admin', }, + screenshot: 'only-on-failure', + permissions: ['clipboard-read', 'clipboard-write'], provisioningRootDir: path.join(process.cwd(), process.env.PROV_DIR ?? 'conf/provisioning'), }, projects: [ @@ -44,7 +49,7 @@ export default defineConfig({ // Run all tests in parallel using user with admin role { name: 'admin', - testDir: path.join(testDirRoot, '/plugin-e2e-api-tests/as-admin-user'), + testDir: path.join(pluginDirRoot, '/plugin-e2e-api-tests/as-admin-user'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -54,7 +59,7 @@ export default defineConfig({ // Run all tests in parallel using user with viewer role { name: 'viewer', - testDir: path.join(testDirRoot, '/plugin-e2e-api-tests/as-viewer-user'), + testDir: path.join(pluginDirRoot, '/plugin-e2e-api-tests/as-viewer-user'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/viewer.json', @@ -63,7 +68,7 @@ export default defineConfig({ }, { name: 'elasticsearch', - testDir: path.join(testDirRoot, '/elasticsearch'), + testDir: path.join(pluginDirRoot, '/elasticsearch'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -72,7 +77,7 @@ export default defineConfig({ }, { name: 'mysql', - testDir: path.join(testDirRoot, '/mysql'), + testDir: path.join(pluginDirRoot, '/mysql'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -81,7 +86,7 @@ export default defineConfig({ }, { name: 'mssql', - testDir: path.join(testDirRoot, '/mssql'), + testDir: path.join(pluginDirRoot, '/mssql'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -90,7 +95,7 @@ export default defineConfig({ }, { name: 'extensions-test-app', - testDir: 'e2e/test-plugins/grafana-extensionstest-app', + testDir: path.join(testDirRoot, '/test-plugins/grafana-extensionstest-app'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -99,7 +104,7 @@ export default defineConfig({ }, { name: 'grafana-e2etest-datasource', - testDir: 'e2e/test-plugins/grafana-test-datasource', + testDir: path.join(testDirRoot, '/test-plugins/grafana-test-datasource'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -108,7 +113,7 @@ export default defineConfig({ }, { name: 'cloudwatch', - testDir: path.join(testDirRoot, '/cloudwatch'), + testDir: path.join(pluginDirRoot, '/cloudwatch'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -117,7 +122,7 @@ export default defineConfig({ }, { name: 'azuremonitor', - testDir: path.join(testDirRoot, '/azuremonitor'), + testDir: path.join(pluginDirRoot, '/azuremonitor'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -126,7 +131,7 @@ export default defineConfig({ }, { name: 'cloudmonitoring', - testDir: path.join(testDirRoot, '/cloudmonitoring'), + testDir: path.join(pluginDirRoot, '/cloudmonitoring'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -135,7 +140,7 @@ export default defineConfig({ }, { name: 'graphite', - testDir: path.join(testDirRoot, '/graphite'), + testDir: path.join(pluginDirRoot, '/graphite'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -144,7 +149,7 @@ export default defineConfig({ }, { name: 'influxdb', - testDir: path.join(testDirRoot, '/influxdb'), + testDir: path.join(pluginDirRoot, '/influxdb'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -153,7 +158,7 @@ export default defineConfig({ }, { name: 'opentsdb', - testDir: path.join(testDirRoot, '/opentsdb'), + testDir: path.join(pluginDirRoot, '/opentsdb'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -162,7 +167,7 @@ export default defineConfig({ }, { name: 'jaeger', - testDir: path.join(testDirRoot, '/jaeger'), + testDir: path.join(pluginDirRoot, '/jaeger'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -171,7 +176,7 @@ export default defineConfig({ }, { name: 'grafana-postgresql-datasource', - testDir: path.join(testDirRoot, '/grafana-postgresql-datasource'), + testDir: path.join(pluginDirRoot, '/grafana-postgresql-datasource'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -180,7 +185,50 @@ export default defineConfig({ }, { name: 'zipkin', - testDir: path.join(testDirRoot, '/zipkin'), + testDir: path.join(pluginDirRoot, '/zipkin'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'scenarios', + testDir: path.join(testDirRoot, '/scenarios'), + use: { + ...devices['Desktop Chrome'], + }, + }, + { + name: 'various', + testDir: path.join(testDirRoot, '/various-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'panels', + testDir: path.join(testDirRoot, '/panels-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'smoke', + testDir: path.join(testDirRoot, '/smoke-tests-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'dashboards', + testDir: path.join(testDirRoot, '/dashboards-suite'), use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json', @@ -196,5 +244,23 @@ export default defineConfig({ }, dependencies: ['authenticate'], }, + { + name: 'cloud-plugins', + testDir: path.join(testDirRoot, '/cloud-plugins-suite'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, + { + name: 'dashboard-new-layouts', + testDir: path.join(testDirRoot, '/dashboard-new-layouts'), + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['authenticate'], + }, ], }); diff --git a/playwright.storybook.config.ts b/playwright.storybook.config.ts new file mode 100644 index 00000000000..faa18ce7d05 --- /dev/null +++ b/playwright.storybook.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; + +import { PluginOptions } from '@grafana/plugin-e2e'; + +const testDirRoot = 'e2e-playwright'; + +export default defineConfig({ + fullyParallel: true, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:9001', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: { + command: 'yarn storybook', + url: 'http://localhost:9001', + stdout: 'ignore', + stderr: 'pipe', + }, + projects: [ + { + name: 'storybook', + testDir: path.join(testDirRoot, '/storybook'), + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}); diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 50b23341467..63175726fd5 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -834,7 +834,7 @@ def playwright_e2e_tests_step(): "commands": [ "npx wait-on@7.0.1 http://$HOST:$PORT", "yarn playwright install --with-deps chromium", - "yarn e2e:playwright", + "yarn e2e:playwright --grep @plugins", ], } diff --git a/scripts/grafana-server/custom.ini b/scripts/grafana-server/custom.ini index 68968c502fa..d8e92d8782e 100644 --- a/scripts/grafana-server/custom.ini +++ b/scripts/grafana-server/custom.ini @@ -1,3 +1,8 @@ +app_mode = development + +[log] +level = error +filters = http.server:info,settings:info [security] content_security_policy = true diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index 6162439a299..6a71bf6e19b 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -41,11 +41,11 @@ echo -e "Copying custom plugins from e2e tests" mkdir -p "$RUNDIR/data/plugins" -if [ -d "./e2e/test-plugins" ]; then - ln -s $(realpath ./e2e/test-plugins/*) "$RUNDIR/data/plugins" +if [ -d "./e2e-playwright/test-plugins" ]; then + ln -s $(realpath ./e2e-playwright/test-plugins/*) "$RUNDIR/data/plugins" # when running in CI -elif [ -d "../e2e/test-plugins" ]; then - cp -r "../e2e/test-plugins" "$RUNDIR/data/plugins" +elif [ -d "../e2e-playwright/test-plugins" ]; then + cp -r "../e2e-playwright/test-plugins" "$RUNDIR/data/plugins" fi if [ "$INSTALL_IMAGE_RENDERER" ]; then @@ -60,17 +60,17 @@ cp devenv/dashboards.yaml $PROV_DIR/dashboards cp devenv/alert_rules.yaml $PROV_DIR/alerting cp devenv/plugins.yaml $PROV_DIR/plugins -cp -r devenv $RUNDIR +cp -r devenv $ -echo -e "Starting Grafana Server port $PORT" +echo -e "Starting Grafana server port $PORT" -$RUNDIR/bin/"$ARCH"grafana-server \ - --homepath=$HOME_PATH \ - --pidfile=$RUNDIR/pid \ - cfg:server.http_port=$PORT \ - cfg:server.router_logging=1 \ - cfg:app_mode=development \ - cfg:enterprise.license_path=$1 - -# 2>&1 > $RUNDIR/output.log & -# cfg:log.level=debug \ +# We get a lot of "context canceled" errors from navigating away from pages, +# so filter them out because they're not useful +{ + exec "$RUNDIR/bin/${ARCH}grafana" server \ + --homepath="$HOME_PATH" \ + --pidfile="$RUNDIR/pid" \ + cfg:server.http_port="$PORT" \ + cfg:enterprise.license_path="$1" \ + 2>&1 +} | grep -v -i "context canceled" diff --git a/yarn.lock b/yarn.lock index 58e016dab57..192b15b61bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8583,9 +8583,9 @@ __metadata: languageName: node linkType: hard -"@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app": +"@test-plugins/extensions-test-app@workspace:e2e-playwright/test-plugins/grafana-extensionstest-app": version: 0.0.0-use.local - resolution: "@test-plugins/extensions-test-app@workspace:e2e/test-plugins/grafana-extensionstest-app" + resolution: "@test-plugins/extensions-test-app@workspace:e2e-playwright/test-plugins/grafana-extensionstest-app" dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" @@ -8615,9 +8615,9 @@ __metadata: languageName: unknown linkType: soft -"@test-plugins/grafana-e2etest-datasource@workspace:e2e/test-plugins/grafana-test-datasource": +"@test-plugins/grafana-e2etest-datasource@workspace:e2e-playwright/test-plugins/grafana-test-datasource": version: 0.0.0-use.local - resolution: "@test-plugins/grafana-e2etest-datasource@workspace:e2e/test-plugins/grafana-test-datasource" + resolution: "@test-plugins/grafana-e2etest-datasource@workspace:e2e-playwright/test-plugins/grafana-test-datasource" dependencies: "@emotion/css": "npm:11.11.2" "@grafana/data": "workspace:*" From 453a791db19b60d7baa0a24ed997301aeb8075f1 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 11 Jul 2025 10:34:24 +0100 Subject: [PATCH 08/25] Internationalisation: More mark up following manual review (#107709) * mark up variable types * mark up annotation stuff * mark up TagsInput placeholder * mark up validation messages * mark up required error * Mark up metrics explorer modal --- .../src/locales/en-US/grafana-prometheus.json | 26 +++++++ .../metrics-modal/AdditionalSettings.tsx | 4 +- .../components/metrics-modal/MetricsModal.tsx | 8 ++- .../components/metrics-modal/ResultsTable.tsx | 17 +++-- .../components/metrics-modal/state/helpers.ts | 69 ++++++++++++++----- .../components/metrics-modal/types.ts | 1 + .../src/components/TagsInput/TagsInput.tsx | 5 +- .../components/AnnotationResultMapper.tsx | 4 +- .../annotations/standardAnnotationSupport.ts | 35 ++++++++-- .../saving/SaveDashboardAsForm.tsx | 5 +- .../annotations/AnnotationSettingsEdit.tsx | 25 ++++--- .../settings/variables/utils.test.ts | 24 ++++--- .../settings/variables/utils.ts | 69 +++++++++++++------ .../AnnotationSettingsEdit.tsx | 25 ++++--- .../services/ValidationSrv.ts | 28 ++++++-- public/locales/en-US/grafana.json | 66 +++++++++++++++++- 16 files changed, 321 insertions(+), 90 deletions(-) diff --git a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json index d7c9b1f6b1b..7885c3d457c 100644 --- a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json @@ -225,6 +225,29 @@ "step": "Step: {{value}}", "type": "Type: {{value}}" }, + "get-placeholders": { + "browse": "Search metrics by name", + "include-null-metadata": "Include results with no metadata", + "metadata-search-switch": "Include description in search", + "set-use-backend": "Enable regex search", + "type": "Filter by type" + }, + "get-prom-types": { + "description-counter": "A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.", + "description-gauge": "A metric that represents a single numerical value that can arbitrarily go up and down.", + "description-histogram": "A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.", + "description-native-histogram": "Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.", + "description-no-type": "These metrics have no defined type in the metadata.", + "description-summary": "A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.", + "description-unknown": "These metrics have been given the type unknown in the metadata.", + "label-counter": "Counter", + "label-gauge": "Gauge", + "label-histogram": "Histogram", + "label-native-histogram": "Native histogram", + "label-no-type": "No type", + "label-summary": "Summary", + "label-unknown": "Unknown" + }, "handle-function": { "text": { "query-parsing-is-ambiguous": "Query parsing is ambiguous." @@ -368,6 +391,9 @@ "results-table": { "content-descriptive-type": "When creating a {{descriptiveType}}, Prometheus exposes multiple series with the type counter. ", "description": "Description", + "message-expand-label-filters": "There are no metrics found. Try to expand your label filters.", + "message-expand-search": "There are no metrics found. Try to expand your search and filters.", + "message-no-metrics-found": "There are no metrics found in the data source.", "name": "Name", "select": "Select", "type": "Type" diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx index a60a070b5b4..4ce04117dd9 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx @@ -7,7 +7,7 @@ import { Icon, Switch, Tooltip, useTheme2 } from '@grafana/ui'; import { metricsModaltestIds } from './shared/testIds'; import { AdditionalSettingsProps } from './shared/types'; -import { placeholders } from './state/helpers'; +import { getPlaceholders } from './state/helpers'; export function AdditionalSettings(props: AdditionalSettingsProps) { const { state, onChangeFullMetaSearch, onChangeIncludeNullMetadata, onChangeDisableTextWrap, onChangeUseBackend } = @@ -16,6 +16,8 @@ export function AdditionalSettings(props: AdditionalSettingsProps) { const theme = useTheme2(); const styles = getStyles(theme); + const placeholders = getPlaceholders(); + return ( <>
diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx index 3d2c500e795..f598cf0951d 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx @@ -32,8 +32,8 @@ import { calculatePageList, calculateResultsPerPage, displayedMetrics, - placeholders, - promTypes, + getPlaceholders, + getPromTypes, setMetrics, tracking, } from './state/helpers'; @@ -55,6 +55,8 @@ export const MetricsModal = (props: MetricsModalProps) => { const theme = useTheme2(); const styles = getStyles(theme, state.disableTextWrap); + const placeholders = getPlaceholders(); + const promTypes = getPromTypes(); /** * loads metrics and metadata on opening modal and switching off useBackend @@ -88,7 +90,7 @@ export const MetricsModal = (props: MetricsModalProps) => { const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => { return { value: t.value, - label: t.value, + label: t.label, description: t.description, }; }); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx index 47c990c091f..1e179d336ee 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx @@ -4,7 +4,7 @@ import { ReactElement } from 'react'; import Highlighter from 'react-highlight-words'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { Button, Icon, Tooltip, useTheme2 } from '@grafana/ui'; import { docsTip } from '../../../configuration/shared/utils'; @@ -106,15 +106,24 @@ export function ResultsTable(props: ResultsTableProps) { let message; if (!state.fuzzySearchQuery) { - message = 'There are no metrics found in the data source.'; + message = t( + 'grafana-prometheus.querybuilder.results-table.message-no-metrics-found', + 'There are no metrics found in the data source.' + ); } if (query.labels.length > 0) { - message = 'There are no metrics found. Try to expand your label filters.'; + message = t( + 'grafana-prometheus.querybuilder.results-table.message-expand-label-filters', + 'There are no metrics found. Try to expand your label filters.' + ); } if (state.fuzzySearchQuery || state.selectedTypes.length > 0) { - message = 'There are no metrics found. Try to expand your search and filters.'; + message = t( + 'grafana-prometheus.querybuilder.results-table.message-expand-search', + 'There are no metrics found. Try to expand your search and filters.' + ); } return ( diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts index 66fa83e2624..88d5f9e27bf 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts @@ -1,6 +1,7 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts import { AnyAction } from '@reduxjs/toolkit'; +import { t } from '@grafana/i18n'; import { reportInteraction } from '@grafana/runtime'; import { PrometheusDatasource } from '../../../../datasource'; @@ -232,45 +233,75 @@ export function tracking(event: string, state?: MetricsModalState | null, metric } } -export const promTypes: PromFilterOption[] = [ +export const getPromTypes: () => PromFilterOption[] = () => [ { value: 'counter', - description: - 'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-counter', 'Counter'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-counter', + 'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.' + ), }, { value: 'gauge', - description: 'A metric that represents a single numerical value that can arbitrarily go up and down.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-gauge', 'Gauge'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-gauge', + 'A metric that represents a single numerical value that can arbitrarily go up and down.' + ), }, { value: 'histogram', - description: - 'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-histogram', 'Histogram'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-histogram', + 'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.' + ), }, { value: 'native histogram', - description: - 'Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-native-histogram', 'Native histogram'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-native-histogram', + 'Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.' + ), }, { value: 'summary', - description: - 'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-summary', 'Summary'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-summary', + 'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.' + ), }, { value: 'unknown', - description: 'These metrics have been given the type unknown in the metadata.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-unknown', 'Unknown'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-unknown', + 'These metrics have been given the type unknown in the metadata.' + ), }, { value: 'no type', - description: 'These metrics have no defined type in the metadata.', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-no-type', 'No type'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-no-type', + 'These metrics have no defined type in the metadata.' + ), }, ]; -export const placeholders = { - browse: 'Search metrics by name', - metadataSearchSwitch: 'Include description in search', - type: 'Filter by type', - includeNullMetadata: 'Include results with no metadata', - setUseBackend: 'Enable regex search', -}; +export const getPlaceholders = () => ({ + browse: t('grafana-prometheus.querybuilder.get-placeholders.browse', 'Search metrics by name'), + metadataSearchSwitch: t( + 'grafana-prometheus.querybuilder.get-placeholders.metadata-search-switch', + 'Include description in search' + ), + type: t('grafana-prometheus.querybuilder.get-placeholders.type', 'Filter by type'), + includeNullMetadata: t( + 'grafana-prometheus.querybuilder.get-placeholders.include-null-metadata', + 'Include results with no metadata' + ), + setUseBackend: t('grafana-prometheus.querybuilder.get-placeholders.set-use-backend', 'Enable regex search'), +}); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts index 233f3765e8a..88d39441eb3 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/types.ts @@ -9,6 +9,7 @@ export type MetricData = { export type PromFilterOption = { value: string; + label: string; description: string; }; diff --git a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx index 808e5245ae0..7bcdbb6852a 100644 --- a/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx +++ b/packages/grafana-ui/src/components/TagsInput/TagsInput.tsx @@ -3,7 +3,7 @@ import { useCallback, useState, forwardRef } from 'react'; import * as React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; import { Button } from '../Button/Button'; @@ -32,7 +32,7 @@ export interface Props { export const TagsInput = forwardRef( ( { - placeholder = 'New tag (enter key to add)', + placeholder: placeholderProp, tags = [], onChange, width, @@ -45,6 +45,7 @@ export const TagsInput = forwardRef( }, ref ) => { + const placeholder = placeholderProp ?? t('grafana-ui.tags-input.placeholder-new-tag', 'New tag (enter key to add)'); const [newTagName, setNewTagName] = useState(''); const styles = useStyles2(getStyles); const theme = useTheme2(); diff --git a/public/app/features/annotations/components/AnnotationResultMapper.tsx b/public/app/features/annotations/components/AnnotationResultMapper.tsx index 475659c7620..b572ab13e40 100644 --- a/public/app/features/annotations/components/AnnotationResultMapper.tsx +++ b/public/app/features/annotations/components/AnnotationResultMapper.tsx @@ -13,7 +13,7 @@ import { import { Trans, t } from '@grafana/i18n'; import { Select, Tooltip, Icon } from '@grafana/ui'; -import { annotationEventNames, AnnotationFieldInfo } from '../standardAnnotationSupport'; +import { getAnnotationEventNames, AnnotationFieldInfo } from '../standardAnnotationSupport'; import { AnnotationQueryResponse } from '../types'; // const valueOptions: Array> = [ @@ -204,7 +204,7 @@ export class AnnotationFieldMapper extends PureComponent {
- {annotationEventNames.map((row) => { + {getAnnotationEventNames().map((row) => { return this.renderRow(row, mappings[row.key] || {}, first); })} diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts index a2caca1d583..90d28715c7f 100644 --- a/public/app/features/annotations/standardAnnotationSupport.ts +++ b/public/app/features/annotations/standardAnnotationSupport.ts @@ -17,6 +17,7 @@ import { KeyValue, standardTransformers, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { config } from 'app/core/config'; export const standardAnnotationSupport: AnnotationSupport = { @@ -97,22 +98,44 @@ export interface AnnotationFieldInfo { } // These fields get added to the standard UI -export const annotationEventNames: AnnotationFieldInfo[] = [ +export const getAnnotationEventNames: () => AnnotationFieldInfo[] = () => [ { key: 'time', field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.time), - placeholder: 'time, or the first time field', + placeholder: t( + 'annotations.get-annotation-event-names.placeholder.time-or-the-first-field', + '{{defaultField}}, or the first time field', + { defaultField: 'time' } + ), + }, + { + key: 'timeEnd', + // label: 'end time', + help: t( + 'annotations.get-annotation-event-names.help.annotation-treated-as-range', + 'When this field is defined, the annotation will be treated as a range' + ), }, - { key: 'timeEnd', label: 'end time', help: 'When this field is defined, the annotation will be treated as a range' }, { key: 'title', }, { key: 'text', field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.string), - placeholder: 'text, or the first text field', + placeholder: t( + 'annotations.get-annotation-event-names.placeholder.text-or-the-first-field', + '{{defaultField}}, or the first text field', + { defaultField: 'text' } + ), + }, + { + key: 'tags', + split: ',', + help: t( + 'annotations.get-annotation-event-names.help.results-split-on-comma', + 'The results will be split on comma (,)' + ), }, - { key: 'tags', split: ',', help: 'The results will be split on comma (,)' }, { key: 'id', }, @@ -134,7 +157,7 @@ export const publicDashboardEventNames: AnnotationFieldInfo[] = [ // pipeline, but include fields that should not be exposed generally const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [ ...(config.publicDashboardAccessToken ? publicDashboardEventNames : []), - ...annotationEventNames, + ...getAnnotationEventNames(), { key: 'userId' }, { key: 'login' }, { key: 'email' }, diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 9c2f783c49e..00001fac1f8 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -118,7 +118,10 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { onSave(false))}> } invalid={!!errors.title} error={errors.title?.message}> <> [ { - label: 'All panels', + label: t('dashboard.get-panel-filters.label.all-panels', 'All panels'), value: PanelFilterType.AllPanels, - description: 'Send the annotation data to all panels that support annotations', + description: t( + 'dashboard.get-panel-filters.description.annotation-panels-support-annotations', + 'Send the annotation data to all panels that support annotations' + ), }, { - label: 'Selected panels', + label: t('dashboard.get-panel-filters.label.selected-panels', 'Selected panels'), value: PanelFilterType.IncludePanels, - description: 'Send the annotations to the explicitly listed panels', + description: t( + 'dashboard.get-panel-filters.description.annotations-explicitly-listed-panels', + 'Send the annotations to the explicitly listed panels' + ), }, { - label: 'All panels except', + label: t('dashboard.get-panel-filters.label.all-panels-except', 'All panels except'), value: PanelFilterType.ExcludePanels, - description: 'Do not send annotation data to the following panels', + description: t( + 'dashboard.get-panel-filters.description.annotation-following-panels', + 'Do not send annotation data to the following panels' + ), }, ]; diff --git a/public/app/features/manage-dashboards/services/ValidationSrv.ts b/public/app/features/manage-dashboards/services/ValidationSrv.ts index 24d41c1c048..48ca75c417b 100644 --- a/public/app/features/manage-dashboards/services/ValidationSrv.ts +++ b/public/app/features/manage-dashboards/services/ValidationSrv.ts @@ -1,3 +1,4 @@ +import { t } from '@grafana/i18n'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; class ValidationError extends Error { @@ -13,14 +14,24 @@ export class ValidationSrv { rootName = 'general'; validateNewDashboardName(folderUID: string, name: string) { - return this.validate(folderUID, name, 'A dashboard or a folder with the same name already exists'); + return this.validate( + folderUID, + name, + t( + 'manage-dashboards.validation-srv.message-same-name', + 'A dashboard or a folder with the same name already exists' + ) + ); } validateNewFolderName(name?: string) { return this.validate( this.rootName, name, - 'A folder or dashboard in the general folder with the same name already exists' + t( + 'manage-dashboards.validation-srv.message-same-name-general', + 'A folder or dashboard in the general folder with the same name already exists' + ) ); } @@ -29,11 +40,20 @@ export class ValidationSrv { const nameLowerCased = name.toLowerCase(); if (name.length === 0) { - throw new ValidationError('REQUIRED', 'Name is required'); + throw new ValidationError( + 'REQUIRED', + t('manage-dashboards.validation-srv.message-name-required', 'Name is required') + ); } if (nameLowerCased === this.rootName) { - throw new ValidationError('EXISTING', 'This is a reserved name and cannot be used for a folder.'); + throw new ValidationError( + 'EXISTING', + t( + 'manage-dashboards.validation-srv.message-reserved-name', + 'This is a reserved name and cannot be used for a folder.' + ) + ); } const searcher = getGrafanaSearcher(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index ab3d742f57d..6daba435bae 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3143,6 +3143,16 @@ "info-box-content-2": "Checkout the <2>Annotations documentation for more information.", "title": "There are no custom annotation queries added yet" }, + "get-annotation-event-names": { + "help": { + "annotation-treated-as-range": "When this field is defined, the annotation will be treated as a range", + "results-split-on-comma": "The results will be split on comma (,)" + }, + "placeholder": { + "text-or-the-first-field": "{{defaultField}}, or the first text field", + "time-or-the-first-field": "{{defaultField}}, or the first time field" + } + }, "standard-annotation-query-editor": { "events-found": "{{numEvents}} events (from {{numFields}} fields)", "no-events-found": "No events found", @@ -4676,6 +4686,18 @@ "transform-data": "Transform data" } }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "Do not send annotation data to the following panels", + "annotation-panels-support-annotations": "Send the annotation data to all panels that support annotations", + "annotations-explicitly-listed-panels": "Send the annotations to the explicitly listed panels" + }, + "label": { + "all-panels": "All panels", + "all-panels-except": "All panels except", + "selected-panels": "Selected panels" + } + }, "get-panel-frame-category": { "descriptor": { "title": { @@ -5581,6 +5603,40 @@ "row-height-options": "Row height options" } }, + "get-editable-variables": { + "description": { + "add-keyvalue-filters-on-the-fly": "Add key/value filters on the fly", + "dynamically-switch-source-multiple-panels": "Dynamically switch the data source for multiple panels", + "group": "Add keys to group by on the fly", + "hidden-constant-variable": "A hidden constant variable, useful for metric prefixes in dashboards you want to share", + "users-enter-arbitrary-strings-textbox": "Users can enter any arbitrary strings in a textbox", + "values-are-static-and-defined-manually": "Values are static and defined manually", + "values-fetched-source-query": "Values are fetched from a data source query", + "values-timespans": "Values are timespans, ex 1m, 1h, 1d" + }, + "name": { + "ad-hoc-filters": "Ad hoc filters", + "constant": "Constant", + "custom": "Custom", + "data-source": "Data source", + "group-by": "Group by", + "interval": "Interval", + "query": "Query", + "textbox": "Textbox" + } + }, + "get-panel-filters": { + "description": { + "annotation-following-panels": "Do not send annotation data to the following panels", + "annotation-panels-support-annotations": "Send the annotation data to all panels that support annotations", + "annotations-explicitly-listed-panels": "Send the annotations to the explicitly listed panels" + }, + "label": { + "all-panels": "All panels", + "all-panels-except": "All panels except", + "selected-panels": "Selected panels" + } + }, "get-panel-frame-options": { "descriptor": { "title": { @@ -5814,7 +5870,8 @@ "label-folder": "Folder", "render-footer": { "title-failed-to-save-dashboard": "Failed to save dashboard" - } + }, + "required": "Required" }, "save-dashboard-drawer": { "tabs": { @@ -8022,6 +8079,7 @@ }, "tags-input": { "add": "Add", + "placeholder-new-tag": "New tag (enter key to add)", "remove": "Remove tag: {{name}}" }, "time-sync-button": { @@ -8968,6 +9026,12 @@ "text": { "import-dashboard": "Import dashboard" } + }, + "validation-srv": { + "message-name-required": "Name is required", + "message-reserved-name": "This is a reserved name and cannot be used for a folder.", + "message-same-name": "A dashboard or a folder with the same name already exists", + "message-same-name-general": "A folder or dashboard in the general folder with the same name already exists" } }, "metric-select": { From 0a0d926531ca3c8f6f25725d268744d06870deab Mon Sep 17 00:00:00 2001 From: Kristian Bremberg <114284895+KristianGrafana@users.noreply.github.com> Date: Fri, 11 Jul 2025 12:13:23 +0200 Subject: [PATCH 09/25] BackendSrv: Validate newline and tabs in validatePath (#108008) Validate newline and tabs in validatePath --- .../grafana-data/src/text/sanitize.test.ts | 21 +++++++++++++++++++ packages/grafana-data/src/text/sanitize.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/grafana-data/src/text/sanitize.test.ts b/packages/grafana-data/src/text/sanitize.test.ts index 218e4064046..c3ce3d3eb90 100644 --- a/packages/grafana-data/src/text/sanitize.test.ts +++ b/packages/grafana-data/src/text/sanitize.test.ts @@ -78,6 +78,27 @@ describe('validatePath', () => { expect(() => validatePath('/api/%/admin')).toThrow(PathValidationError); expect(() => validatePath('/api/%2/admin')).toThrow(PathValidationError); }); + + it('should block paths with tab characters', () => { + expect(() => validatePath('/api/\tadmin')).toThrow(PathValidationError); + expect(() => validatePath('/api/users\t/123')).toThrow(PathValidationError); + }); + + it('should block paths with newline characters', () => { + expect(() => validatePath('/api/\nadmin')).toThrow(PathValidationError); + expect(() => validatePath('/api/users\n/123')).toThrow(PathValidationError); + }); + + it('should block paths with carriage return characters', () => { + expect(() => validatePath('/api/\radmin')).toThrow(PathValidationError); + expect(() => validatePath('/api/users\r/123')).toThrow(PathValidationError); + }); + + it('should block URL encoded tab and newline characters', () => { + expect(() => validatePath('/api/%09admin')).toThrow(PathValidationError); // tab + expect(() => validatePath('/api/%0Aadmin')).toThrow(PathValidationError); // newline + expect(() => validatePath('/api/%0Dadmin')).toThrow(PathValidationError); // carriage return + }); }); describe('safe paths', () => { diff --git a/packages/grafana-data/src/text/sanitize.ts b/packages/grafana-data/src/text/sanitize.ts index 8fed4311627..0c3dadd5ebf 100644 --- a/packages/grafana-data/src/text/sanitize.ts +++ b/packages/grafana-data/src/text/sanitize.ts @@ -151,7 +151,7 @@ export function validatePath(path: OriginalPath): O originalDecoded = cleaned; // If the original string contains traversal attempts, block it - if (originalDecoded.includes('..') || originalDecoded.includes('/\\')) { + if (/\.\.|\/\\|[\t\n\r]/.test(originalDecoded)) { throw new PathValidationError(); } From 66b79fb2211e3ac1c200f7a9c8a5170a64827468 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 11 Jul 2025 11:13:35 +0100 Subject: [PATCH 10/25] Internationalisation: Mark up transformations (#107910) * CalculateFieldTransformerEditor * more CalculateFieldTransformer * ConcatenateTransformerEditor * ConfigFromQueryTransformerEditor * mark up other transformations * few things spotted manually --- .betterer.results | 4 - apps/iam/pkg/apis/iam_manifest.go | 2 - .../transformSceneToSaveModel.test.ts | 4 +- .../TransformationPickerNg.tsx | 49 +-- .../FilterByValueTransformerEditor.tsx | 23 +- .../HeatmapTransformerEditor.tsx | 23 +- .../transformers/calculateHeatmap/heatmap.ts | 14 +- .../ConfigFromQueryTransformerEditor.tsx | 26 +- .../configFromQuery/configFromQuery.ts | 12 +- .../CalculateFieldTransformerEditor.tsx | 77 ++-- .../CalculateFieldTransformerEditor/index.ts | 6 - .../editors/ConcatenateTransformerEditor.tsx | 54 ++- .../ConvertFieldTypeTransformerEditor.tsx | 24 +- .../editors/FilterByNameTransformerEditor.tsx | 22 +- .../FilterByRefIdTransformerEditor.tsx | 15 +- .../editors/FormatStringTransformerEditor.tsx | 22 +- .../editors/FormatTimeTransformerEditor.tsx | 22 +- .../editors/GroupByTransformerEditor.tsx | 27 +- .../GroupToNestedTableTransformerEditor.tsx | 49 ++- .../GroupingToMatrixTransformerEditor.tsx | 22 +- .../editors/HistogramTransformerEditor.tsx | 11 +- .../editors/JoinByFieldTransformerEditor.tsx | 58 +-- .../LabelsToFieldsTransformerEditor.tsx | 28 +- .../editors/LimitTransformerEditor.tsx | 11 +- .../editors/MergeTransformerEditor.tsx | 14 +- .../OrganizeFieldsTransformerEditor.tsx | 23 +- .../editors/ReduceTransformerEditor.tsx | 11 +- .../editors/RenameByRegexTransformer.tsx | 22 +- .../editors/SeriesToRowsTransformerEditor.tsx | 24 +- .../editors/SortByTransformerEditor.tsx | 8 +- .../editors/TransposeTransformerEditor.tsx | 17 +- .../ExtractFieldsTransformerEditor.tsx | 11 +- .../JoinByLabelsTransformerEditor.tsx | 25 +- .../transformers/joinByLabels/joinByLabels.ts | 14 +- .../FieldLookupTransformerEditor.tsx | 14 +- .../lookupGazetteer/fieldLookup.ts | 2 +- .../PartitionByValuesEditor.tsx | 26 +- .../partitionByValues.test.ts | 4 +- .../partitionByValues/partitionByValues.ts | 55 +-- .../PrepareTimeSeriesEditor.tsx | 206 ++++++----- .../prepareTimeSeries.test.ts | 4 +- .../prepareTimeSeries/prepareTimeSeries.ts | 14 +- .../regression/regression.test.ts | 4 +- .../transformers/regression/regression.ts | 13 +- .../regression/regressionEditor.tsx | 21 +- .../RowsToFieldsTransformerEditor.tsx | 23 +- .../transformers/rowsToFields/rowsToFields.ts | 12 +- .../spatial/SpatialTransformerEditor.tsx | 23 +- .../spatial/spatialTransformer.ts | 12 +- .../transformers/standardTransformers.ts | 132 +++---- .../TimeSeriesTableTransformEditor.tsx | 24 +- .../timeSeriesTableTransformer.ts | 18 +- public/app/features/transformers/utils.ts | 24 +- public/locales/en-US/grafana.json | 347 +++++++++++++++++- 54 files changed, 1145 insertions(+), 607 deletions(-) delete mode 100644 public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts diff --git a/.betterer.results b/.betterer.results index 8a98e82ff57..db0480152ab 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2785,10 +2785,6 @@ exports[`better eslint`] = { "public/app/features/transformers/editors/CalculateFieldTransformerEditor/WindowOptionsEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts:5381": [ - [0, 0, 0, "Do not re-export imported variable (\`CalculateFieldTransformerEditor\`)", "0"], - [0, 0, 0, "Do not re-export imported variable (\`calculateFieldTransformRegistryItem\`)", "1"] - ], "public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/apps/iam/pkg/apis/iam_manifest.go b/apps/iam/pkg/apis/iam_manifest.go index 8f144c3cd27..667b7a10354 100644 --- a/apps/iam/pkg/apis/iam_manifest.go +++ b/apps/iam/pkg/apis/iam_manifest.go @@ -14,8 +14,6 @@ import ( v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" ) -var () - var appManifestData = app.ManifestData{ AppName: "iam", Group: "iam.grafana.app", diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 9d4fbea9aa2..b543bdeb10b 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -19,7 +19,7 @@ import { MultiValueVariable, sceneGraph, SceneGridRow, VizPanel } from '@grafana import { Dashboard, LoadingState, Panel, RowPanel, VariableRefresh } from '@grafana/schema'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; -import { reduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor'; +import { getReduceTransformRegistryItem } from 'app/features/transformers/editors/ReduceTransformerEditor'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; import { DashboardDataDTO } from 'app/types/dashboard'; @@ -46,7 +46,7 @@ import { trimDashboardForSnapshot, } from './transformSceneToSaveModel'; -standardTransformersRegistry.setInit(() => [reduceTransformRegistryItem]); +standardTransformersRegistry.setInit(() => [getReduceTransformRegistryItem()]); setPluginImportUtils({ importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})), getPanelPluginFromCache: (id: string) => undefined, diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx index 4dab1da8b45..368cfb23e1c 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx @@ -3,7 +3,6 @@ import { FormEventHandler, KeyboardEventHandler, ReactNode, useCallback } from ' import { DataFrame, - DataTransformerID, TransformerRegistryItem, TransformationApplicabilityLevels, GrafanaTheme2, @@ -15,17 +14,11 @@ import { Trans, t } from '@grafana/i18n'; import { Badge, Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2 } from '@grafana/ui'; import config from 'app/core/config'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; -import { categoriesLabels } from 'app/features/transformers/utils'; +import { getCategoriesLabels } from 'app/features/transformers/utils'; import { FilterCategory } from './TransformationsEditor'; -const viewAllLabel = 'View all'; const VIEW_ALL_VALUE = 'viewAll'; -const filterCategoriesLabels: Array<[FilterCategory, string]> = [ - [VIEW_ALL_VALUE, viewAllLabel], - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - ...(Object.entries(categoriesLabels) as Array<[FilterCategory, string]>), -]; interface TransformationPickerNgProps { onTransformationAdd: (selectedItem: SelectableValue) => void; @@ -60,6 +53,12 @@ export function TransformationPickerNg(props: TransformationPickerNgProps) { onSelectedFilterChange, } = props; + const filterCategoriesLabels: Array<[FilterCategory, string]> = [ + [VIEW_ALL_VALUE, t('dashboard.transformation-picker-ng.view-all', 'View all')], + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + ...(Object.entries(getCategoriesLabels()) as Array<[FilterCategory, string]>), + ]; + // Use a callback ref to call "click" on the search input // This will focus it when it's opened const searchInputRef = useCallback((input: HTMLInputElement) => { @@ -218,18 +217,13 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data {transform.tags && transform.tags.size > 0 && (
{Array.from(transform.tags).map((tag) => ( - + ))}
)} - {getTransformationsRedesignDescriptions(transform.id)} + {standardTransformersRegistry.getIfExists(transform.id)?.description} {showIllustrations && ( {transform.name} @@ -319,28 +313,3 @@ const getImagePath = (id: string, disabled: boolean) => { const folder = config.theme2.isDark ? 'dark' : 'light'; return `public/build/img/transformations/${folder}/${id}.svg`; }; - -const TransformationDescriptionOverrides: { [key: string]: string } = { - [DataTransformerID.concatenate]: 'Combine all fields into a single frame.', - [DataTransformerID.configFromData]: 'Set unit, min, max and more.', - [DataTransformerID.fieldLookup]: 'Use a field value to lookup countries, states, or airports.', - [DataTransformerID.filterFieldsByName]: 'Remove parts of the query results using a regex pattern.', - [DataTransformerID.filterByRefId]: 'Remove rows from the data based on origin query', - [DataTransformerID.filterByValue]: 'Remove rows from the query results using user-defined filters.', - [DataTransformerID.groupBy]: 'Group data by a field value and create aggregate data.', - [DataTransformerID.groupingToMatrix]: 'Summarize and reorganize data based on three fields.', - [DataTransformerID.joinByField]: 'Combine rows from 2+ tables, based on a related field.', - [DataTransformerID.labelsToFields]: 'Group series by time and return labels or tags as fields.', - [DataTransformerID.merge]: 'Merge multiple series. Values will be combined into one row.', - [DataTransformerID.organize]: 'Re-order, hide, or rename fields.', - [DataTransformerID.partitionByValues]: 'Split a one-frame dataset into multiple series.', - [DataTransformerID.prepareTimeSeries]: 'Stretch data frames from the wide format into the long format.', - [DataTransformerID.reduce]: 'Reduce all rows or data points to a single value (ex. max, mean).', - [DataTransformerID.renameByRegex]: - 'Rename parts of the query results using a regular expression and replacement pattern.', - [DataTransformerID.seriesToRows]: 'Merge multiple series. Return time, metric and values as a row.', -}; - -const getTransformationsRedesignDescriptions = (id: string): string => { - return TransformationDescriptionOverrides[id] || standardTransformersRegistry.getIfExists(id)?.description || ''; -}; diff --git a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx index 13bc7163c88..fb255cbb530 100644 --- a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx @@ -148,16 +148,19 @@ export const FilterByValueTransformerEditor = (props: TransformerUIProps = { - id: DataTransformerID.filterByValue, - editor: FilterByValueTransformerEditor, - transformation: standardTransformers.filterByValueTransformer, - name: standardTransformers.filterByValueTransformer.name, - description: - 'Removes rows of the query results using user-defined filters. This is useful if you can not filter your data in the data source.', - categories: new Set([TransformerCategory.Filter]), - help: getTransformationContent(DataTransformerID.filterByValue).helperDocs, -}; +export const getFilterByValueTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.filterByValue, + editor: FilterByValueTransformerEditor, + transformation: standardTransformers.filterByValueTransformer, + name: t('transformers.filter-by-value-transformer-editor.name.filter-data-by-values', 'Filter data by values'), + description: t( + 'transformers.filter-by-value-transformer-editor.description.remove-rows-query-results-user-defined-filters', + 'Remove rows from the query results using user-defined filters.' + ), + categories: new Set([TransformerCategory.Filter]), + help: getTransformationContent(DataTransformerID.filterByValue).helperDocs, + }); const useFieldsInfo = (data: DataFrame[]): DataFrameFieldsInfo => { return useMemo(() => { diff --git a/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx b/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx index a8810d11807..01d3cdd8064 100644 --- a/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx +++ b/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx @@ -13,7 +13,7 @@ import { getTransformationContent } from '../docs/getTransformationContent'; import { getDefaultOptions, getTransformerOptionPane } from '../spatial/optionsHelper'; import { addHeatmapCalculationOptions } from './editor/helper'; -import { HeatmapTransformerOptions, heatmapTransformer } from './heatmap'; +import { HeatmapTransformerOptions, getHeatmapTransformer } from './heatmap'; // Nothing defined in state const supplier = ( @@ -42,13 +42,16 @@ export const HeatmapTransformerEditor = (props: TransformerUIProps = { - id: heatmapTransformer.id, - editor: HeatmapTransformerEditor, - transformation: heatmapTransformer, - name: heatmapTransformer.name, - description: heatmapTransformer.description, - state: PluginState.alpha, - categories: new Set([TransformerCategory.CreateNewVisualization]), - help: getTransformationContent(heatmapTransformer.id).helperDocs, +export const getHeatmapTransformRegistryItem: () => TransformerRegistryItem = () => { + const heatmapTransformer = getHeatmapTransformer(); + return { + id: heatmapTransformer.id, + editor: HeatmapTransformerEditor, + transformation: heatmapTransformer, + name: heatmapTransformer.name, + description: heatmapTransformer.description, + state: PluginState.alpha, + categories: new Set([TransformerCategory.CreateNewVisualization]), + help: getTransformationContent(heatmapTransformer.id).helperDocs, + }; }; diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index ed37d76651e..7968444302d 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -16,6 +16,7 @@ import { TimeRange, } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { ScaleDistribution, HeatmapCellLayout, @@ -30,10 +31,13 @@ export interface HeatmapTransformerOptions extends HeatmapCalculationOptions { keepOriginalData?: boolean; } -export const heatmapTransformer: SynchronousDataTransformerInfo = { +export const getHeatmapTransformer: () => SynchronousDataTransformerInfo = () => ({ id: DataTransformerID.heatmap, - name: 'Create heatmap', - description: 'Generate heatmap data from source data.', + name: t('transformers.get-heatmap-transformer.name.create-heatmap', 'Create heatmap'), + description: t( + 'transformers.get-heatmap-transformer.description.generate-heatmap-data-from-source', + 'Generate heatmap data from source data.' + ), defaultOptions: {}, isApplicable: (data) => { const { xField, yField, xs, ys } = findHeatmapFields(data); @@ -51,7 +55,7 @@ export const heatmapTransformer: SynchronousDataTransformerInfo (source) => - source.pipe(map((data) => heatmapTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getHeatmapTransformer().transformer(options, ctx)(data))), transformer: (options: HeatmapTransformerOptions) => { return (data: DataFrame[]) => { @@ -62,7 +66,7 @@ export const heatmapTransformer: SynchronousDataTransformerInfo {} @@ -98,16 +98,20 @@ export function ConfigFromQueryTransformerEditor({ input, onChange, options }: P ); } -export const configFromQueryTransformRegistryItem: TransformerRegistryItem = { - id: configFromDataTransformer.id, - editor: ConfigFromQueryTransformerEditor, - transformation: configFromDataTransformer, - name: configFromDataTransformer.name, - description: configFromDataTransformer.description, - state: PluginState.beta, - categories: new Set([TransformerCategory.CalculateNewFields]), - help: getTransformationContent(configFromDataTransformer.id).helperDocs, -}; +export const getConfigFromQueryTransformRegistryItem: () => TransformerRegistryItem = + () => { + const configFromDataTransformer = getConfigFromDataTransformer(); + return { + id: configFromDataTransformer.id, + editor: ConfigFromQueryTransformerEditor, + transformation: configFromDataTransformer, + name: configFromDataTransformer.name, + description: configFromDataTransformer.description, + state: PluginState.beta, + categories: new Set([TransformerCategory.CalculateNewFields]), + help: getTransformationContent(configFromDataTransformer.id).helperDocs, + }; + }; const getStyles = (theme: GrafanaTheme2) => ({ matcherOptions: css({ diff --git a/public/app/features/transformers/configFromQuery/configFromQuery.ts b/public/app/features/transformers/configFromQuery/configFromQuery.ts index 44cd15c2117..4e8ae24b311 100644 --- a/public/app/features/transformers/configFromQuery/configFromQuery.ts +++ b/public/app/features/transformers/configFromQuery/configFromQuery.ts @@ -10,6 +10,7 @@ import { MatcherConfig, reduceField, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { evaluateFieldMappings, @@ -89,10 +90,13 @@ export function extractConfigFromQuery(options: ConfigFromQueryTransformOptions, return output; } -export const configFromDataTransformer: DataTransformerInfo = { +export const getConfigFromDataTransformer: () => DataTransformerInfo = () => ({ id: DataTransformerID.configFromData, - name: 'Config from query results', - description: 'Set unit, min, max and more from data.', + name: t('transformers.get-config-from-data-transformer.name.config-from-query-results', 'Config from query results'), + description: t( + 'transformers.get-config-from-data-transformer.description.set-unit-min-max-and-more', + 'Set unit, min, max and more.' + ), defaultOptions: { configRefId: 'config', mappings: [], @@ -103,4 +107,4 @@ export const configFromDataTransformer: DataTransformerInfo (source) => source.pipe(map((data) => extractConfigFromQuery(options, data))), -}; +}); diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx index 55566e530a1..28d020d4e44 100644 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx @@ -42,20 +42,6 @@ interface CalculateFieldTransformerEditorState { selected: string[]; } -const calculationModes = [ - { value: CalculateFieldMode.BinaryOperation, label: 'Binary operation' }, - { value: CalculateFieldMode.UnaryOperation, label: 'Unary operation' }, - { value: CalculateFieldMode.ReduceRow, label: 'Reduce row' }, - { value: CalculateFieldMode.Index, label: 'Row index' }, -]; - -if (cfg.featureToggles.addFieldFromCalculationStatFunctions) { - calculationModes.push( - { value: CalculateFieldMode.CumulativeFunctions, label: 'Cumulative functions' }, - { value: CalculateFieldMode.WindowFunctions, label: 'Window functions' } - ); -} - const okTypes = new Set([FieldType.time, FieldType.number, FieldType.string, FieldType.boolean]); export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformerEditorProps) => { @@ -63,6 +49,44 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer const configuredOptions = options?.reduce?.include; const [state, setState] = useState({ names: [], selected: [] }); + const calculationModes = [ + { + value: CalculateFieldMode.BinaryOperation, + label: t( + 'transformers.calculate-field-transformer-editor.calculation-modes.label.binary-operation', + 'Binary operation' + ), + }, + { + value: CalculateFieldMode.UnaryOperation, + label: t( + 'transformers.calculate-field-transformer-editor.calculation-modes.label.unary-operation', + 'Unary operation' + ), + }, + { + value: CalculateFieldMode.ReduceRow, + label: t('transformers.calculate-field-transformer-editor.calculation-modes.label.reduce-row', 'Reduce row'), + }, + { + value: CalculateFieldMode.Index, + label: t('transformers.calculate-field-transformer-editor.calculation-modes.label.row-index', 'Row index'), + }, + ]; + + if (cfg.featureToggles.addFieldFromCalculationStatFunctions) { + calculationModes.push( + { + value: CalculateFieldMode.CumulativeFunctions, + label: t('transformers.calculate-field-transformer-editor.label.cumulative-functions', 'Cumulative functions'), + }, + { + value: CalculateFieldMode.WindowFunctions, + label: t('transformers.calculate-field-transformer-editor.label.window-functions', 'Window functions'), + } + ); + } + useEffect(() => { const ctx = { interpolate: (v: string) => v }; const subscription = of(input) @@ -240,12 +264,19 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer ); }; -export const calculateFieldTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.calculateField, - editor: CalculateFieldTransformerEditor, - transformation: standardTransformers.calculateFieldTransformer, - name: standardTransformers.calculateFieldTransformer.name, - description: 'Use the row values to calculate a new field.', - categories: new Set([TransformerCategory.CalculateNewFields]), - help: getTransformationContent(DataTransformerID.calculateField).helperDocs, -}; +export const getCalculateFieldTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.calculateField, + editor: CalculateFieldTransformerEditor, + transformation: standardTransformers.calculateFieldTransformer, + name: t( + 'transformers.get-calculate-field-transform-registry-item.name.add-field-from-calculation', + 'Add field from calculation' + ), + description: t( + 'transformers.get-calculate-field-transform-registry-item.description.values-calculate-field', + 'Use the row values to calculate a new field.' + ), + categories: new Set([TransformerCategory.CalculateNewFields]), + help: getTransformationContent(DataTransformerID.calculateField).helperDocs, + }); diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts deleted file mode 100644 index 0772bb9cd00..00000000000 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { - CalculateFieldTransformerEditor, - calculateFieldTransformRegistryItem, -} from './CalculateFieldTransformerEditor'; - -export { CalculateFieldTransformerEditor, calculateFieldTransformRegistryItem }; diff --git a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx index d106bbfadeb..1318cf4de8c 100644 --- a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx @@ -16,12 +16,6 @@ import { getTransformationContent } from '../docs/getTransformationContent'; interface ConcatenateTransformerEditorProps extends TransformerUIProps {} -const nameModes: Array> = [ - { value: ConcatenateFrameNameMode.FieldName, label: 'Copy frame name to field name' }, - { value: ConcatenateFrameNameMode.Label, label: 'Add a label with the frame name' }, - { value: ConcatenateFrameNameMode.Drop, label: 'Ignore the frame name' }, -]; - export class ConcatenateTransformerEditor extends PureComponent { constructor(props: ConcatenateTransformerEditorProps) { super(props); @@ -50,6 +44,29 @@ export class ConcatenateTransformerEditor extends PureComponent> = [ + { + value: ConcatenateFrameNameMode.FieldName, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.copy-frame-name-to-field', + 'Copy frame name to field name' + ), + }, + { + value: ConcatenateFrameNameMode.Label, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.label-frame', + 'Add a label with the frame name' + ), + }, + { + value: ConcatenateFrameNameMode.Drop, + label: t( + 'transformers.concatenate-transformer-editor.name-modes.label.ignore-the-frame-name', + 'Ignore the frame name' + ), + }, + ]; const frameNameMode = options.frameNameMode ?? ConcatenateFrameNameMode.FieldName; @@ -82,14 +99,17 @@ export class ConcatenateTransformerEditor extends PureComponent = { - id: DataTransformerID.concatenate, - editor: ConcatenateTransformerEditor, - transformation: standardTransformers.concatenateTransformer, - name: standardTransformers.concatenateTransformer.name, - description: - 'Combine all fields into a single frame. Values will be appended with undefined values if not the same length.', - categories: new Set([TransformerCategory.Combine]), - help: getTransformationContent(DataTransformerID.concatenate).helperDocs, - tags: new Set(['Combine']), -}; +export const getConcatenateTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.concatenate, + editor: ConcatenateTransformerEditor, + transformation: standardTransformers.concatenateTransformer, + name: t('transformers.editors.concatenate-transformer-editor.name.concatenate-fields', 'Concatenate fields'), + description: t( + 'transformers.editors.concatenate-transformer-editor.description.combine-all-fields', + 'Combine all fields into a single frame.' + ), + categories: new Set([TransformerCategory.Combine]), + help: getTransformationContent(DataTransformerID.concatenate).helperDocs, + tags: new Set([t('transformers.editors.concatenate-transformer-editor.tags.combine', 'Combine')]), + }); diff --git a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx index 14fddeba815..0a1542f033a 100644 --- a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx @@ -267,13 +267,17 @@ export const ConvertFieldTypeTransformerEditor = ({ ); }; -export const convertFieldTypeTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.convertFieldType, - editor: ConvertFieldTypeTransformerEditor, - transformation: standardTransformers.convertFieldTypeTransformer, - name: standardTransformers.convertFieldTypeTransformer.name, - description: standardTransformers.convertFieldTypeTransformer.description, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.convertFieldType).helperDocs, - tags: new Set(['Format field']), -}; +export const getConvertFieldTypeTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.convertFieldType, + editor: ConvertFieldTypeTransformerEditor, + transformation: standardTransformers.convertFieldTypeTransformer, + name: t('transformers.convert-field-type-transformer-editor.name.convert-field-type', 'Convert field type'), + description: t( + 'transformers.convert-field-type-transformer-editor.description.convert-to-specified-field-type', + 'Convert a field to a specified field type.' + ), + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.convertFieldType).helperDocs, + tags: new Set([t('transformers.convert-field-type-transformer-editor.tags.format-field', 'Format field')]), + }); diff --git a/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx b/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx index c68ac0dbe41..9f7f37f2e59 100644 --- a/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx @@ -253,12 +253,16 @@ export class FilterByNameTransformerEditor extends React.PureComponent< } } -export const filterFieldsByNameTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.filterFieldsByName, - editor: FilterByNameTransformerEditor, - transformation: standardTransformers.filterFieldsByNameTransformer, - name: standardTransformers.filterFieldsByNameTransformer.name, - description: 'Removes part of the query results using a regex pattern. The pattern can be inclusive or exclusive.', - categories: new Set([TransformerCategory.Filter]), - help: getTransformationContent(DataTransformerID.filterFieldsByName).helperDocs, -}; +export const getFilterFieldsByNameTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.filterFieldsByName, + editor: FilterByNameTransformerEditor, + transformation: standardTransformers.filterFieldsByNameTransformer, + name: t('transformers.filter-by-name-transformer-editor.name.filter-fields-by-name', 'Filter fields by name'), + description: t( + 'transformers.filter-by-name-transformer-editor.description.remove-part-query-results-regex-pattern', + 'Remove parts of the query results using a regex pattern.' + ), + categories: new Set([TransformerCategory.Filter]), + help: getTransformationContent(DataTransformerID.filterFieldsByName).helperDocs, + }); diff --git a/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx b/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx index 05b9e196b83..ad679578389 100644 --- a/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx @@ -7,6 +7,7 @@ import { FrameMatcherID, } from '@grafana/data'; import { FilterFramesByRefIdTransformerOptions } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { FrameMultiSelectionEditor } from 'app/plugins/panel/geomap/editor/FrameSelectionEditor'; import { getTransformationContent } from '../docs/getTransformationContent'; @@ -29,14 +30,16 @@ export const FilterByRefIdTransformerEditor = (props: TransformerUIProps = - { +export const getFilterFramesByRefIdTransformRegistryItem: () => TransformerRegistryItem = + () => ({ id: DataTransformerID.filterByRefId, editor: FilterByRefIdTransformerEditor, transformation: standardTransformers.filterFramesByRefIdTransformer, - name: standardTransformers.filterFramesByRefIdTransformer.name, - description: - 'Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel.', + name: t('transformers.filter-by-ref-id-transformer-editor.name.filter-data-by-query', 'Filter data by query'), + description: t( + 'transformers.filter-by-ref-id-transformer-editor.description.filter-data-by-query-useful-sharing-results', + 'Remove rows from the data based on origin query' + ), categories: new Set([TransformerCategory.Filter]), help: getTransformationContent(DataTransformerID.filterByRefId).helperDocs, - }; + }); diff --git a/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx b/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx index 4e23322f9ed..e29cc29833e 100644 --- a/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx @@ -119,12 +119,16 @@ function FormatStringTransfomerEditor({ ); } -export const formatStringTransformerRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.formatString, - editor: FormatStringTransfomerEditor, - transformation: standardTransformers.formatStringTransformer, - name: standardTransformers.formatStringTransformer.name, - state: PluginState.beta, - description: standardTransformers.formatStringTransformer.description, - categories: new Set([TransformerCategory.Reformat]), -}; +export const getFormatStringTransformerRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.formatString, + editor: FormatStringTransfomerEditor, + transformation: standardTransformers.formatStringTransformer, + name: t('transformers.format-string-transformer-editor.name.format-string', 'Format string'), + state: PluginState.beta, + description: t( + 'transformers.format-string-transformer-editor.description.manipulate-string-fields-formatting', + 'Manipulate string fields formatting.' + ), + categories: new Set([TransformerCategory.Reformat]), + }); diff --git a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx index 82304ce8870..ca749cfa5c0 100644 --- a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx @@ -117,12 +117,16 @@ export function FormatTimeTransfomerEditor({ ); } -export const formatTimeTransformerRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.formatTime, - editor: FormatTimeTransfomerEditor, - transformation: standardTransformers.formatTimeTransformer, - name: standardTransformers.formatTimeTransformer.name, - state: PluginState.alpha, - description: standardTransformers.formatTimeTransformer.description, - help: getTransformationContent(DataTransformerID.formatTime).helperDocs, -}; +export const getFormatTimeTransformerRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.formatTime, + editor: FormatTimeTransfomerEditor, + transformation: standardTransformers.formatTimeTransformer, + name: t('transformers.format-time-transformer-editor.name.format-time', 'Format time'), + state: PluginState.alpha, + description: t( + 'transformers.format-time-transformer-editor.description.set-based-on-time', + 'Set the output format of a time field' + ), + help: getTransformationContent(DataTransformerID.formatTime).helperDocs, + }); diff --git a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx index 91c01232de2..fcde5f473de 100644 --- a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx @@ -86,11 +86,6 @@ export const GroupByTransformerEditor = ({ ); }; -const options = [ - { label: 'Group by', value: GroupByOperationID.groupBy }, - { label: 'Calculate', value: GroupByOperationID.aggregate }, -]; - export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldProps) => { const theme = useTheme2(); @@ -106,6 +101,17 @@ export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: [config, onConfigChange] ); + const options = [ + { + label: t('transformers.group-by-field-configuration.options.label.group-by', 'Group by'), + value: GroupByOperationID.groupBy, + }, + { + label: t('transformers.group-by-field-configuration.options.label.calculate', 'Calculate'), + value: GroupByOperationID.aggregate, + }, + ]; + return ( @@ -153,16 +159,19 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export const groupByTransformRegistryItem: TransformerRegistryItem = { +export const getGroupByTransformRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.groupBy, editor: GroupByTransformerEditor, transformation: standardTransformers.groupByTransformer, - name: standardTransformers.groupByTransformer.name, - description: standardTransformers.groupByTransformer.description, + name: t('transformers.group-by-transformer-editor.name.group-by', 'Group by'), + description: t( + 'transformers.group-by-transformer-editor.description.group-series-by-field-calculate-stats', + 'Group data by a field value and create aggregate data.' + ), categories: new Set([ TransformerCategory.Combine, TransformerCategory.CalculateNewFields, TransformerCategory.Reformat, ]), help: getTransformationContent(DataTransformerID.groupBy).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx b/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx index f0c0b01a007..288fd8441e9 100644 --- a/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupToNestedTableTransformerEditor.tsx @@ -123,11 +123,6 @@ export const GroupToNestedTableTransformerEditor = ({ ); }; -const options = [ - { label: 'Group by', value: GroupByOperationID.groupBy }, - { label: 'Calculate', value: GroupByOperationID.aggregate }, -]; - export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldProps) => { const theme = useTheme2(); const styles = getStyles(theme); @@ -141,6 +136,17 @@ export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: [config, onConfigChange] ); + const options = [ + { + label: t('transformers.group-by-field-configuration.options.label.group-by', 'Group by'), + value: GroupByOperationID.groupBy, + }, + { + label: t('transformers.group-by-field-configuration.options.label.calculate', 'Calculate'), + value: GroupByOperationID.aggregate, + }, + ]; + return ( @@ -187,16 +193,23 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export const groupToNestedTableTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.groupToNestedTable, - editor: GroupToNestedTableTransformerEditor, - transformation: standardTransformers.groupToNestedTable, - name: standardTransformers.groupToNestedTable.name, - description: standardTransformers.groupToNestedTable.description, - categories: new Set([ - TransformerCategory.Combine, - TransformerCategory.CalculateNewFields, - TransformerCategory.Reformat, - ]), - state: PluginState.beta, -}; +export const getGroupToNestedTableTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.groupToNestedTable, + editor: GroupToNestedTableTransformerEditor, + transformation: standardTransformers.groupToNestedTable, + name: t( + 'transformers.group-to-nested-table-transformer-editor.name.group-to-nested-tables', + 'Group to nested tables' + ), + description: t( + 'transformers.group-to-nested-table-transformer-editor.description.group-by-field-value', + 'Group data by a field value and create nested tables with the grouped data.' + ), + categories: new Set([ + TransformerCategory.Combine, + TransformerCategory.CalculateNewFields, + TransformerCategory.Reformat, + ]), + state: PluginState.beta, + }); diff --git a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx index 60cae734a2a..9ab69c466b2 100644 --- a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx @@ -148,12 +148,16 @@ export const GroupingToMatrixTransformerEditor = ({ ); }; -export const groupingToMatrixTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.groupingToMatrix, - editor: GroupingToMatrixTransformerEditor, - transformation: standardTransformers.groupingToMatrixTransformer, - name: standardTransformers.groupingToMatrixTransformer.name, - description: 'Takes a three fields combination and produces a Matrix.', - categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.groupingToMatrix).helperDocs, -}; +export const getGroupingToMatrixTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.groupingToMatrix, + editor: GroupingToMatrixTransformerEditor, + transformation: standardTransformers.groupingToMatrixTransformer, + name: t('transformers.grouping-to-matrix-transformer-editor.name.grouping-to-matrix', 'Grouping to matrix'), + description: t( + 'transformers.grouping-to-matrix-transformer-editor.description.summarize-and-reorganize-data', + 'Summarize and reorganize data based on three fields.' + ), + categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.groupingToMatrix).helperDocs, + }); diff --git a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx index c70c8e9244c..02e79b66e8a 100644 --- a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx +++ b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx @@ -136,12 +136,15 @@ export const HistogramTransformerEditor = ({ ); }; -export const histogramTransformRegistryItem: TransformerRegistryItem = { +export const getHistogramTransformRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.histogram, editor: HistogramTransformerEditor, transformation: standardTransformers.histogramTransformer, - name: standardTransformers.histogramTransformer.name, - description: standardTransformers.histogramTransformer.description, + name: t('transformers.histogram-transformer-editor.name.histogram', 'Histogram'), + description: t( + 'transformers.histogram-transformer-editor.description.calculate-histogram-from-input-data', + 'Calculate a histogram from input data.' + ), categories: new Set([TransformerCategory.CreateNewVisualization]), help: getTransformationContent(DataTransformerID.histogram).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx index 244cea37072..8e491297739 100644 --- a/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx @@ -16,30 +16,37 @@ import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/internal'; import { getTransformationContent } from '../docs/getTransformationContent'; -const modes = [ - { - value: JoinMode.outer, - label: 'OUTER (TIME SERIES)', - description: - 'Keep all rows from any table with a value. Join on distinct field values. Performant and best used for time series.', - }, - { - value: JoinMode.outerTabular, - label: 'OUTER (TABULAR)', - description: - 'Join on a field value with duplicated values. Non performant outer join best used for tabular(SQL like) data.', - }, - { - value: JoinMode.inner, - label: 'INNER', - description: 'Combine data from two tables whenever there are matching values in a fields common to both tables.', - }, -]; - export function SeriesToFieldsTransformerEditor({ input, options, onChange }: TransformerUIProps) { const names = useFieldDisplayNames(input); const fieldNames = useSelectOptions(names); + const modes = [ + { + value: JoinMode.outer, + label: t('transformers.series-to-fields-transformer-editor.modes.label.outer-time-series', 'Outer (time series)'), + description: t( + 'transformers.series-to-fields-transformer-editor.modes.description.keep-all-rows', + 'Keep all rows from any table with a value. Join on distinct field values. Performant and best used for time series.' + ), + }, + { + value: JoinMode.outerTabular, + label: t('transformers.series-to-fields-transformer-editor.modes.label.outer-tabular', 'Outer (tabular)'), + description: t( + 'transformers.series-to-fields-transformer-editor.modes.description.join-on-a-field', + 'Join on a field value with duplicated values. Non performant outer join best used for tabular(SQL like) data.' + ), + }, + { + value: JoinMode.inner, + label: t('transformers.series-to-fields-transformer-editor.modes.label.inner', 'Inner'), + description: t( + 'transformers.series-to-fields-transformer-editor.modes.description.combine-data-from-two-tables', + 'Combine data from two tables whenever there are matching values in a fields common to both tables.' + ), + }, + ]; + const variables = getTemplateSrv() .getVariables() .map((v) => { @@ -98,13 +105,16 @@ export function SeriesToFieldsTransformerEditor({ input, options, onChange }: Tr ); } -export const joinByFieldTransformerRegistryItem: TransformerRegistryItem = { +export const getJoinByFieldTransformerRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.joinByField, aliasIds: [DataTransformerID.seriesToColumns], editor: SeriesToFieldsTransformerEditor, transformation: standardTransformers.joinByFieldTransformer, - name: standardTransformers.joinByFieldTransformer.name, - description: standardTransformers.joinByFieldTransformer.description, + name: t('transformers.join-by-field-transformer-editor.name.join-by-field', 'Join by field'), + description: t( + 'transformers.join-by-field-transformer-editor.description.combine-rows-from-2-tables', + 'Combine rows from 2+ tables, based on a related field.' + ), categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(DataTransformerID.joinByField).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx b/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx index bf1eb69bf72..939dacd5763 100644 --- a/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx @@ -14,11 +14,6 @@ import { InlineField, InlineFieldRow, RadioButtonGroup, Select, FilterPill, Stac import { getTransformationContent } from '../docs/getTransformationContent'; -const modes: Array> = [ - { value: LabelsToFieldsMode.Columns, label: 'Columns' }, - { value: LabelsToFieldsMode.Rows, label: 'Rows' }, -]; - export const LabelsAsFieldsTransformerEditor = ({ input, options, @@ -26,6 +21,17 @@ export const LabelsAsFieldsTransformerEditor = ({ }: TransformerUIProps) => { const labelWidth = 20; + const modes: Array> = [ + { + value: LabelsToFieldsMode.Columns, + label: t('transformers.labels-as-fields-transformer-editor.modes.label.columns', 'Columns'), + }, + { + value: LabelsToFieldsMode.Rows, + label: t('transformers.labels-as-fields-transformer-editor.modes.label.rows', 'Rows'), + }, + ]; + const { labelNames, selected } = useMemo(() => { let labelNames: Array> = []; let uniqueLabels: Record = {}; @@ -133,13 +139,15 @@ export const LabelsAsFieldsTransformerEditor = ({ ); }; -export const labelsToFieldsTransformerRegistryItem: TransformerRegistryItem = { +export const getLabelsToFieldsTransformerRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.labelsToFields, editor: LabelsAsFieldsTransformerEditor, transformation: standardTransformers.labelsToFieldsTransformer, - name: standardTransformers.labelsToFieldsTransformer.name, - description: `Groups series by time and return labels or tags as fields. - Useful for showing time series with labels in a table where each label key becomes a separate column.`, + name: t('transformers.labels-to-fields-transformer-editor.name.labels-to-fields', 'Labels to fields'), + description: t( + 'transformers.labels-to-fields-transformer-editor.description.groups-series-time-return-labels-tags-fields', + 'Group series by time and return labels or tags as fields.' + ), categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.labelsToFields).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/LimitTransformerEditor.tsx b/public/app/features/transformers/editors/LimitTransformerEditor.tsx index 65313152a49..326f02f3c82 100644 --- a/public/app/features/transformers/editors/LimitTransformerEditor.tsx +++ b/public/app/features/transformers/editors/LimitTransformerEditor.tsx @@ -45,12 +45,15 @@ export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps ); }; -export const limitTransformRegistryItem: TransformerRegistryItem = { +export const getLimitTransformRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.limit, editor: LimitTransformerEditor, transformation: standardTransformers.limitTransformer, - name: standardTransformers.limitTransformer.name, - description: `Limit the number of items displayed.`, + name: t('transformers.limit-transformer-editor.name.limit', 'Limit'), + description: t( + 'transformers.limit-transformer-editor.description.limit-number-items-displayed', + 'Limit the number of items displayed.' + ), categories: new Set([TransformerCategory.Filter]), help: getTransformationContent(DataTransformerID.limit).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/MergeTransformerEditor.tsx b/public/app/features/transformers/editors/MergeTransformerEditor.tsx index 99666ad801e..25ba613307a 100644 --- a/public/app/features/transformers/editors/MergeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/MergeTransformerEditor.tsx @@ -6,7 +6,7 @@ import { TransformerCategory, } from '@grafana/data'; import { MergeTransformerOptions } from '@grafana/data/internal'; -import { Trans } from '@grafana/i18n'; +import { Trans, t } from '@grafana/i18n'; import { FieldValidationMessage } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; @@ -25,13 +25,15 @@ export const MergeTransformerEditor = ({ input, options, onChange }: Transformer return null; }; -export const mergeTransformerRegistryItem: TransformerRegistryItem = { +export const getMergeTransformerRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.merge, editor: MergeTransformerEditor, transformation: standardTransformers.mergeTransformer, - name: standardTransformers.mergeTransformer.name, - description: `Merge many series/tables and return a single table where mergeable values will be combined into the same row. - Useful for showing multiple series, tables or a combination of both visualized in a table.`, + name: t('transformers.merge-transformer-editor.name.merge', 'Merge series/tables'), + description: t( + 'transformers.merge-transformer-editor.description.merge-multiple-series', + 'Merge multiple series. Values will be combined into one row.' + ), categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(DataTransformerID.merge).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx index 48c18bdc493..7eec24ee6c6 100644 --- a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx @@ -495,13 +495,16 @@ const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record = { - id: DataTransformerID.organize, - editor: OrganizeFieldsTransformerEditor, - transformation: standardTransformers.organizeFieldsTransformer, - name: standardTransformers.organizeFieldsTransformer.name, - description: - "Allows the user to re-order, hide, or rename fields / columns. Useful when data source doesn't allow overrides for visualizing data.", - categories: new Set([TransformerCategory.ReorderAndRename]), - help: getTransformationContent(DataTransformerID.organize).helperDocs, -}; +export const getOrganizeFieldsTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.organize, + editor: OrganizeFieldsTransformerEditor, + transformation: standardTransformers.organizeFieldsTransformer, + name: t('transformers.organize-fields-transformer-editor.name.organize-fields', 'Organize fields by name'), + description: t( + 'transformers.organize-fields-transformer-editor.description.reorder-hide-or-rename-fields', + 'Re-order, hide, or rename fields.' + ), + categories: new Set([TransformerCategory.ReorderAndRename]), + help: getTransformationContent(DataTransformerID.organize).helperDocs, + }); diff --git a/public/app/features/transformers/editors/ReduceTransformerEditor.tsx b/public/app/features/transformers/editors/ReduceTransformerEditor.tsx index fe69d0a474d..3df242e370c 100644 --- a/public/app/features/transformers/editors/ReduceTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ReduceTransformerEditor.tsx @@ -117,12 +117,15 @@ export const ReduceTransformerEditor = ({ options, onChange }: TransformerUIProp ); }; -export const reduceTransformRegistryItem: TransformerRegistryItem = { +export const getReduceTransformRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.reduce, editor: ReduceTransformerEditor, transformation: standardTransformers.reduceTransformer, - name: standardTransformers.reduceTransformer.name, - description: standardTransformers.reduceTransformer.description, + name: t('transformers.reduce-transformer-editor.name.reduce', 'Reduce'), + description: t( + 'transformers.reduce-transformer-editor.description.reduce-to-single-value', + 'Reduce all rows or data points to a single value (ex. max, mean).' + ), categories: new Set([TransformerCategory.CalculateNewFields]), help: getTransformationContent(DataTransformerID.reduce).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/RenameByRegexTransformer.tsx b/public/app/features/transformers/editors/RenameByRegexTransformer.tsx index c68bdf998f9..554557e9804 100644 --- a/public/app/features/transformers/editors/RenameByRegexTransformer.tsx +++ b/public/app/features/transformers/editors/RenameByRegexTransformer.tsx @@ -120,12 +120,16 @@ export class RenameByRegexTransformerEditor extends React.PureComponent< } } -export const renameByRegexTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.renameByRegex, - editor: RenameByRegexTransformerEditor, - transformation: standardTransformers.renameByRegexTransformer, - name: standardTransformers.renameByRegexTransformer.name, - description: 'Renames part of the query result by using regular expression with placeholders.', - categories: new Set([TransformerCategory.ReorderAndRename]), - help: getTransformationContent(DataTransformerID.renameByRegex).helperDocs, -}; +export const getRenameByRegexTransformRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.renameByRegex, + editor: RenameByRegexTransformerEditor, + transformation: standardTransformers.renameByRegexTransformer, + name: t('transformers.rename-by-regex-transformer.name.rename-fields-by-regex', 'Rename fields by regex'), + description: t( + 'transformers.rename-by-regex-transformer.description.rename-parts-using-regex', + 'Rename parts of the query results using a regular expression and replacement pattern.' + ), + categories: new Set([TransformerCategory.ReorderAndRename]), + help: getTransformationContent(DataTransformerID.renameByRegex).helperDocs, + }); diff --git a/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx b/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx index 8b3b66e64ed..15ead54a4a3 100644 --- a/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx @@ -6,6 +6,7 @@ import { TransformerCategory, } from '@grafana/data'; import { SeriesToRowsTransformerOptions } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { getTransformationContent } from '../docs/getTransformationContent'; @@ -17,13 +18,16 @@ export const SeriesToRowsTransformerEditor = ({ return null; }; -export const seriesToRowsTransformerRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.seriesToRows, - editor: SeriesToRowsTransformerEditor, - transformation: standardTransformers.seriesToRowsTransformer, - name: standardTransformers.seriesToRowsTransformer.name, - description: `Merge many series and return a single series with time, metric and value as columns. - Useful for showing multiple time series visualized in a table.`, - categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.seriesToRows).helperDocs, -}; +export const getSeriesToRowsTransformerRegistryItem: () => TransformerRegistryItem = + () => ({ + id: DataTransformerID.seriesToRows, + editor: SeriesToRowsTransformerEditor, + transformation: standardTransformers.seriesToRowsTransformer, + name: t('transformers.series-to-rows-transformer-editor.name.series-to-rows', 'Series to rows'), + description: t( + 'transformers.series-to-rows-transformer-editor.description.merge-multiple-series', + 'Merge multiple series. Return time, metric and values as a row.' + ), + categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.seriesToRows).helperDocs, + }); diff --git a/public/app/features/transformers/editors/SortByTransformerEditor.tsx b/public/app/features/transformers/editors/SortByTransformerEditor.tsx index b3cd482e83d..3f76977c92f 100644 --- a/public/app/features/transformers/editors/SortByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SortByTransformerEditor.tsx @@ -64,12 +64,12 @@ export const SortByTransformerEditor = ({ input, options, onChange }: Transforme ); }; -export const sortByTransformRegistryItem: TransformerRegistryItem = { +export const getSortByTransformRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.sortBy, editor: SortByTransformerEditor, transformation: standardTransformers.sortByTransformer, - name: standardTransformers.sortByTransformer.name, - description: standardTransformers.sortByTransformer.description, + name: t('transformers.sort-by-transformer-editor.name.sort-by', 'Sort by'), + description: t('transformers.sort-by-transformer-editor.description.sort-fields', 'Sort fields in a frame.'), categories: new Set([TransformerCategory.ReorderAndRename]), help: getTransformationContent(DataTransformerID.sortBy).helperDocs, -}; +}); diff --git a/public/app/features/transformers/editors/TransposeTransformerEditor.tsx b/public/app/features/transformers/editors/TransposeTransformerEditor.tsx index fd9b5c332c5..d8902f20beb 100644 --- a/public/app/features/transformers/editors/TransposeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/TransposeTransformerEditor.tsx @@ -43,12 +43,19 @@ export const TransposeTransformerEditor = ({ options, onChange }: TransformerUIP ); }; -export const transposeTransformerRegistryItem: TransformerRegistryItem = { +export const getTransposeTransformerRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.transpose, editor: TransposeTransformerEditor, transformation: standardTransformers.transposeTransformer, - name: standardTransformers.transposeTransformer.name, - description: standardTransformers.transposeTransformer.description, + name: t('transformers.transpose-transformer-editor.name.transpose', 'Transpose'), + description: t( + 'transformers.transpose-transformer-editor.description.transpose-data-frame', + 'Transpose the data frame.' + ), categories: new Set([TransformerCategory.Reformat]), - tags: new Set(['Pivot', 'Translate', 'Transform']), -}; + tags: new Set([ + t('transformers.transpose-transformer-editor.tags.pivot', 'Pivot'), + t('transformers.transpose-transformer-editor.tags.translate', 'Translate'), + t('transformers.transpose-transformer-editor.tags.transform', 'Transform'), + ]), +}); diff --git a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx index e03cfb22c7b..a8b85f39c5c 100644 --- a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx @@ -180,12 +180,15 @@ export const extractFieldsTransformerEditor = ({ ); }; -export const extractFieldsTransformRegistryItem: TransformerRegistryItem = { +export const getExtractFieldsTransformRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.extractFields, editor: extractFieldsTransformerEditor, transformation: extractFieldsTransformer, - name: extractFieldsTransformer.name, - description: `Parse fields from content (JSON, labels, etc).`, + name: t('transformers.extract-fields-transformer-editor.name.extract-fields', 'Extract fields'), + description: t( + 'transformers.extract-fields-transformer-editor.description.parse-fields-from-content', + 'Parse fields from content (JSON, labels, etc).' + ), categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.extractFields).helperDocs, -}; +}); diff --git a/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx b/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx index 144f09552fb..a32f09bf478 100644 --- a/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx +++ b/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx @@ -14,7 +14,7 @@ import { Alert, HorizontalGroup, InlineField, InlineFieldRow, Select, ValuePicke import { getTransformationContent } from '../docs/getTransformationContent'; import { getDistinctLabels } from '../utils'; -import { joinByLabelsTransformer, JoinByLabelsTransformOptions } from './joinByLabels'; +import { getJoinByLabelsTransformer, JoinByLabelsTransformOptions } from './joinByLabels'; export interface Props extends TransformerUIProps {} @@ -108,7 +108,7 @@ export function JoinByLabelsTransformerEditor({ input, options, onChange }: Prop = { - id: joinByLabelsTransformer.id, - editor: JoinByLabelsTransformerEditor, - transformation: joinByLabelsTransformer, - name: joinByLabelsTransformer.name, - description: joinByLabelsTransformer.description, - state: PluginState.beta, - categories: new Set([TransformerCategory.Combine]), - help: getTransformationContent(joinByLabelsTransformer.id).helperDocs, +export const getJoinByLabelsTransformRegistryItem: () => TransformerRegistryItem = () => { + const joinByLabelsTransformer = getJoinByLabelsTransformer(); + return { + id: joinByLabelsTransformer.id, + editor: JoinByLabelsTransformerEditor, + transformation: joinByLabelsTransformer, + name: joinByLabelsTransformer.name, + description: joinByLabelsTransformer.description, + state: PluginState.beta, + categories: new Set([TransformerCategory.Combine]), + help: getTransformationContent(joinByLabelsTransformer.id).helperDocs, + }; }; diff --git a/public/app/features/transformers/joinByLabels/joinByLabels.ts b/public/app/features/transformers/joinByLabels/joinByLabels.ts index 4133dae20a6..9d6d8c99772 100644 --- a/public/app/features/transformers/joinByLabels/joinByLabels.ts +++ b/public/app/features/transformers/joinByLabels/joinByLabels.ts @@ -1,6 +1,7 @@ import { map } from 'rxjs/operators'; import { DataFrame, DataTransformerID, Field, FieldType, SynchronousDataTransformerInfo } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { getDistinctLabels } from '../utils'; @@ -9,14 +10,17 @@ export interface JoinByLabelsTransformOptions { join?: string[]; } -export const joinByLabelsTransformer: SynchronousDataTransformerInfo = { +export const getJoinByLabelsTransformer: () => SynchronousDataTransformerInfo = () => ({ id: DataTransformerID.joinByLabels, - name: 'Join by labels', - description: 'Flatten labeled results into a table joined by labels.', + name: t('transformers.get-join-by-labels-transformer.name.join-by-labels', 'Join by labels'), + description: t( + 'transformers.get-join-by-labels-transformer.description.flatten-labeled-results-table-joined-labels', + 'Flatten labeled results into a table joined by labels.' + ), defaultOptions: {}, operator: (options, ctx) => (source) => - source.pipe(map((data) => joinByLabelsTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getJoinByLabelsTransformer().transformer(options, ctx)(data))), transformer: (options: JoinByLabelsTransformOptions) => { return (data: DataFrame[]) => { @@ -26,7 +30,7 @@ export const joinByLabelsTransformer: SynchronousDataTransformerInfo = { +export const getFieldLookupTransformRegistryItem: () => TransformerRegistryItem = () => ({ id: DataTransformerID.fieldLookup, editor: FieldLookupTransformerEditor, transformation: fieldLookupTransformer, - name: fieldLookupTransformer.name, - description: `Use a field value to lookup additional fields from an external source. This currently supports spatial data, but will eventually support more formats.`, + name: t( + 'transformers.field-lookup-transformer-editor.name.lookup-fields-from-resource', + 'Lookup fields from resource' + ), + description: t( + 'transformers.field-lookup-transformer-editor.description.lookup-additional-fields-external-source', + 'Use a field value to lookup countries, states, or airports.' + ), state: PluginState.alpha, categories: new Set([TransformerCategory.PerformSpatialOperations]), help: getTransformationContent(DataTransformerID.fieldLookup).helperDocs, -}; +}); diff --git a/public/app/features/transformers/lookupGazetteer/fieldLookup.ts b/public/app/features/transformers/lookupGazetteer/fieldLookup.ts index 871d06edf05..33aae7ca869 100644 --- a/public/app/features/transformers/lookupGazetteer/fieldLookup.ts +++ b/public/app/features/transformers/lookupGazetteer/fieldLookup.ts @@ -19,7 +19,7 @@ export interface FieldLookupOptions { export const fieldLookupTransformer: DataTransformerInfo = { id: DataTransformerID.fieldLookup, name: 'Lookup fields from resource', - description: 'Retrieve matching data based on specified field', + description: 'Use a field value to lookup countries, states, or airports.', defaultOptions: {}, operator: (options) => (source) => source.pipe(mergeMap((data) => from(doGazetteerXform(data, options)))), diff --git a/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx b/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx index 8c552199900..b974a7d4025 100644 --- a/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx +++ b/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx @@ -22,7 +22,7 @@ import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/internal'; import { getTransformationContent } from '../docs/getTransformationContent'; -import { partitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; +import { getPartitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; export function PartitionByValuesEditor({ input, @@ -178,13 +178,17 @@ export function PartitionByValuesEditor({ ); } -export const partitionByValuesTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.partitionByValues, - editor: PartitionByValuesEditor, - transformation: partitionByValuesTransformer, - name: partitionByValuesTransformer.name, - description: partitionByValuesTransformer.description, - state: PluginState.alpha, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(DataTransformerID.partitionByValues).helperDocs, -}; +export const getPartitionByValuesTransformRegistryItem: () => TransformerRegistryItem = + () => { + const partitionByValuesTransformer = getPartitionByValuesTransformer(); + return { + id: DataTransformerID.partitionByValues, + editor: PartitionByValuesEditor, + transformation: partitionByValuesTransformer, + name: partitionByValuesTransformer.name, + description: partitionByValuesTransformer.description, + state: PluginState.alpha, + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(DataTransformerID.partitionByValues).helperDocs, + }; + }; diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts index 3741ff8ea94..337efd10215 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts @@ -1,12 +1,14 @@ import { toDataFrame, FieldType } from '@grafana/data'; -import { partitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; +import { getPartitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; const ctx = { interpolate: (v: string) => v, }; describe('Partition by values transformer', () => { + const partitionByValuesTransformer = getPartitionByValuesTransformer(); + it('should partition by one field', () => { const source = [ toDataFrame({ diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.ts b/public/app/features/transformers/partitionByValues/partitionByValues.ts index 4ab8d360cbe..e0da0630cc0 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.ts @@ -9,6 +9,7 @@ import { FieldMatcher, } from '@grafana/data'; import { getMatcherConfig, noopTransformer } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { partition } from './partition'; @@ -62,35 +63,39 @@ function buildFieldLabels(names: string[], values: unknown[]) { return labels; } -export const partitionByValuesTransformer: SynchronousDataTransformerInfo = { - id: DataTransformerID.partitionByValues, - name: 'Partition by values', - description: `Splits a one-frame dataset into multiple series discriminated by unique/enum values in one or more fields.`, - defaultOptions: { - keepFields: false, - }, +export const getPartitionByValuesTransformer: () => SynchronousDataTransformerInfo = + () => ({ + id: DataTransformerID.partitionByValues, + name: t('transformers.get-partition-by-values-transformer.name.partition-by-values', 'Partition by values'), + description: t( + 'transformers.get-partition-by-values-transformer.description.split-oneframe-dataset-multiple-series', + 'Split a one-frame dataset into multiple series.' + ), + defaultOptions: { + keepFields: false, + }, - operator: (options, ctx) => (source) => - source.pipe(map((data) => partitionByValuesTransformer.transformer(options, ctx)(data))), + operator: (options, ctx) => (source) => + source.pipe(map((data) => getPartitionByValuesTransformer().transformer(options, ctx)(data))), - transformer: (options: PartitionByValuesTransformerOptions, ctx: DataTransformContext) => { - const matcherConfig = getMatcherConfig({ names: options.fields }); + transformer: (options: PartitionByValuesTransformerOptions, ctx: DataTransformContext) => { + const matcherConfig = getMatcherConfig({ names: options.fields }); - if (!matcherConfig) { - return noopTransformer.transformer({}, ctx); - } - - const matcher = getFieldMatcher(matcherConfig); - - return (data: DataFrame[]) => { - if (!data.length) { - return data; + if (!matcherConfig) { + return noopTransformer.transformer({}, ctx); } - // error if > 1 frame? - return partitionByValues(data[0], matcher, options); - }; - }, -}; + + const matcher = getFieldMatcher(matcherConfig); + + return (data: DataFrame[]) => { + if (!data.length) { + return data; + } + // error if > 1 frame? + return partitionByValues(data[0], matcher, options); + }; + }, + }); // Split a single frame dataset into multiple frames based on values in a set of fields export function partitionByValues( diff --git a/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx b/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx index 19399e3e595..d720744a070 100644 --- a/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx +++ b/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx @@ -14,96 +14,110 @@ import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; -import { prepareTimeSeriesTransformer, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries'; - -const wideInfo = { - label: 'Wide time series', - value: timeSeriesFormat.TimeSeriesWide, - description: 'Creates a single frame joined by time', - info: ( -
    -
  • - Single frame -
  • -
  • - 1st field is shared time field -
  • -
  • - Time in ascending order -
  • -
  • - - Multiple value fields of any type - -
  • -
- ), -}; - -const multiInfo = { - label: 'Multi-frame time series', - value: timeSeriesFormat.TimeSeriesMulti, - description: 'Creates a new frame for each time/number pair', - info: ( -
    -
  • - Multiple frames -
  • -
  • - - Each frame has two fields: {'{{field1}}'}, {'{{field2}}'} - -
  • -
  • - Time in ascending order -
  • -
  • - - String values are represented as labels - -
  • -
  • - All values are numeric -
  • -
- ), -}; - -const longInfo = { - label: 'Long time series', - value: timeSeriesFormat.TimeSeriesLong, - description: 'Convert each frame to long format', - info: ( -
    -
  • - Single frame -
  • -
  • - 1st field is time field -
  • -
  • - - Time in ascending order, but may have duplicates - -
  • -
  • - - String values are represented as separate fields rather than as labels - -
  • -
  • - Multiple value fields may exist -
  • -
- ), -}; - -const formats: Array> = [wideInfo, multiInfo, longInfo]; +import { PrepareTimeSeriesOptions, timeSeriesFormat, getPrepareTimeSeriesTransformer } from './prepareTimeSeries'; export function PrepareTimeSeriesEditor(props: TransformerUIProps): React.ReactElement { const { options, onChange } = props; const styles = useStyles2(getStyles); + const wideInfo = { + label: t('transformers.prepare-time-series-editor.wide-info.label.wide-time-series', 'Wide time series'), + value: timeSeriesFormat.TimeSeriesWide, + description: t( + 'transformers.prepare-time-series-editor.wide-info.description.creates-single-frame-joined', + 'Creates a single frame joined by time' + ), + info: ( +
    +
  • + Single frame +
  • +
  • + 1st field is shared time field +
  • +
  • + Time in ascending order +
  • +
  • + + Multiple value fields of any type + +
  • +
+ ), + }; + + const multiInfo = { + label: t( + 'transformers.prepare-time-series-editor.multi-info.label.multiframe-time-series', + 'Multi-frame time series' + ), + value: timeSeriesFormat.TimeSeriesMulti, + description: t( + 'transformers.prepare-time-series-editor.multi-info.description.creates-frame-timenumber', + 'Creates a new frame for each time/number pair' + ), + info: ( +
    +
  • + Multiple frames +
  • +
  • + + Each frame has two fields: {'{{field1}}'}, {'{{field2}}'} + +
  • +
  • + Time in ascending order +
  • +
  • + + String values are represented as labels + +
  • +
  • + All values are numeric +
  • +
+ ), + }; + + const longInfo = { + label: t('transformers.prepare-time-series-editor.long-info.label.long-time-series', 'Long time series'), + value: timeSeriesFormat.TimeSeriesLong, + description: t( + 'transformers.prepare-time-series-editor.long-info.description.convert-each-frame-to-long-format', + 'Convert each frame to long format' + ), + info: ( +
    +
  • + Single frame +
  • +
  • + 1st field is time field +
  • +
  • + + Time in ascending order, but may have duplicates + +
  • +
  • + + String values are represented as separate fields rather than as labels + +
  • +
  • + + Multiple value fields may exist + +
  • +
+ ), + }; + + const formats: Array> = [wideInfo, multiInfo, longInfo]; + const onSelectFormat = useCallback( (value: SelectableValue) => { onChange({ @@ -154,12 +168,16 @@ const getStyles = (theme: GrafanaTheme2) => ({ }), }); -export const prepareTimeseriesTransformerRegistryItem: TransformerRegistryItem = { - id: prepareTimeSeriesTransformer.id, - editor: PrepareTimeSeriesEditor, - transformation: prepareTimeSeriesTransformer, - name: prepareTimeSeriesTransformer.name, - description: prepareTimeSeriesTransformer.description, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(prepareTimeSeriesTransformer.id).helperDocs, -}; +export const getPrepareTimeseriesTransformerRegistryItem: () => TransformerRegistryItem = + () => { + const prepareTimeSeriesTransformer = getPrepareTimeSeriesTransformer(); + return { + id: prepareTimeSeriesTransformer.id, + editor: PrepareTimeSeriesEditor, + transformation: prepareTimeSeriesTransformer, + name: prepareTimeSeriesTransformer.name, + description: prepareTimeSeriesTransformer.description, + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(prepareTimeSeriesTransformer.id).helperDocs, + }; + }; diff --git a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts index 7184e16ff91..f96dc4946a4 100644 --- a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts +++ b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.test.ts @@ -8,13 +8,15 @@ import { getFrameDisplayName, } from '@grafana/data'; -import { prepareTimeSeriesTransformer, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries'; +import { getPrepareTimeSeriesTransformer, PrepareTimeSeriesOptions, timeSeriesFormat } from './prepareTimeSeries'; const ctx = { interpolate: (v: string) => v, }; describe('Prepare time series transformer', () => { + const prepareTimeSeriesTransformer = getPrepareTimeSeriesTransformer(); + it('should transform wide to multi', () => { const source = [ toDataFrame({ diff --git a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts index fc94e308ec7..a5924958bbc 100644 --- a/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts +++ b/public/app/features/transformers/prepareTimeSeries/prepareTimeSeries.ts @@ -12,6 +12,7 @@ import { Field, MutableDataFrame, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { Labels } from 'app/types/unified-alerting-dto'; import { partitionByValues } from '../partitionByValues/partitionByValues'; @@ -299,14 +300,17 @@ export function longToMultiTimeSeries(frame: DataFrame): DataFrame[] { }); } -export const prepareTimeSeriesTransformer: SynchronousDataTransformerInfo = { +export const getPrepareTimeSeriesTransformer: () => SynchronousDataTransformerInfo = () => ({ id: DataTransformerID.prepareTimeSeries, - name: 'Prepare time series', - description: `Will stretch data frames from the wide format into the long format. This is really helpful to be able to keep backwards compatibility for panels not supporting the new wide format.`, + name: t('transformers.prepare-time-series.name.prepare-time-series', 'Prepare time series'), + description: t( + 'transformers.prepare-time-series.description.stretch-data-frames', + 'Stretch data frames from the wide format into the long format.' + ), defaultOptions: {}, operator: (options, ctx) => (source) => - source.pipe(map((data) => prepareTimeSeriesTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getPrepareTimeSeriesTransformer().transformer(options, ctx)(data))), transformer: (options: PrepareTimeSeriesOptions) => { const format = options?.format ?? timeSeriesFormat.TimeSeriesWide; @@ -349,4 +353,4 @@ export const prepareTimeSeriesTransformer: SynchronousDataTransformerInfo { + const RegressionTransformer = getRegressionTransformer(); + it('it should predict a linear regression to exactly fit the data when the data is f(x) = x', () => { const source = [ toDataFrame({ diff --git a/public/app/features/transformers/regression/regression.ts b/public/app/features/transformers/regression/regression.ts index b7d532d381c..cda498b5e9b 100644 --- a/public/app/features/transformers/regression/regression.ts +++ b/public/app/features/transformers/regression/regression.ts @@ -35,12 +35,15 @@ export const DEGREES = [ { label: () => t('transformers.regression-transformer-editor.label.quintic', 'Quintic'), value: 5 }, ]; -export const RegressionTransformer: SynchronousDataTransformerInfo = { +export const getRegressionTransformer: () => SynchronousDataTransformerInfo = () => ({ id: DataTransformerID.regression, - name: 'Regression analysis', - description: 'Create a new data frame containing values predicted by a statistical model.', + name: t('transformers.regression.name.regression-analysis', 'Regression analysis'), + description: t( + 'transformers.regression.description.create-new-data-frame', + 'Create a new data frame containing values predicted by a statistical model.' + ), operator: (options, ctx) => (source) => - source.pipe(map((data) => RegressionTransformer.transformer(options, ctx)(data))), + source.pipe(map((data) => getRegressionTransformer().transformer(options, ctx)(data))), transformer: (options, ctx) => { return (frames: DataFrame[]) => { const { predictionCount, modelType, degree } = { ...DEFAULTS, ...options }; @@ -137,4 +140,4 @@ export const RegressionTransformer: SynchronousDataTransformerInfo = { - id: DataTransformerID.regression, - editor: RegressionTransformerEditor, - transformation: RegressionTransformer, - name: RegressionTransformer.name, - description: RegressionTransformer.description, - categories: new Set([TransformerCategory.CalculateNewFields]), - help: getTransformationContent(DataTransformerID.regression).helperDocs, +export const getRegressionTransformerRegistryItem: () => TransformerRegistryItem = () => { + const regressionTransformer = getRegressionTransformer(); + return { + id: DataTransformerID.regression, + editor: RegressionTransformerEditor, + transformation: regressionTransformer, + name: regressionTransformer.name, + description: regressionTransformer.description, + categories: new Set([TransformerCategory.CalculateNewFields]), + help: getTransformationContent(DataTransformerID.regression).helperDocs, + }; }; diff --git a/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx b/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx index 8e4a0b5faad..0eb8532f840 100644 --- a/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx @@ -3,7 +3,7 @@ import { PluginState, TransformerRegistryItem, TransformerUIProps, TransformerCa import { getTransformationContent } from '../docs/getTransformationContent'; import { FieldToConfigMappingEditor } from '../fieldToConfigMapping/FieldToConfigMappingEditor'; -import { rowsToFieldsTransformer, RowToFieldsTransformOptions } from './rowsToFields'; +import { getRowsToFieldsTransformer, RowToFieldsTransformOptions } from './rowsToFields'; export interface Props extends TransformerUIProps {} @@ -24,13 +24,16 @@ export function RowsToFieldsTransformerEditor({ input, options, onChange }: Prop ); } -export const rowsToFieldsTransformRegistryItem: TransformerRegistryItem = { - id: rowsToFieldsTransformer.id, - editor: RowsToFieldsTransformerEditor, - transformation: rowsToFieldsTransformer, - name: rowsToFieldsTransformer.name, - description: rowsToFieldsTransformer.description, - state: PluginState.beta, - categories: new Set([TransformerCategory.Reformat]), - help: getTransformationContent(rowsToFieldsTransformer.id).helperDocs, +export const getRowsToFieldsTransformRegistryItem: () => TransformerRegistryItem = () => { + const rowsToFieldsTransformer = getRowsToFieldsTransformer(); + return { + id: rowsToFieldsTransformer.id, + editor: RowsToFieldsTransformerEditor, + transformation: rowsToFieldsTransformer, + name: rowsToFieldsTransformer.name, + description: rowsToFieldsTransformer.description, + state: PluginState.beta, + categories: new Set([TransformerCategory.Reformat]), + help: getTransformationContent(rowsToFieldsTransformer.id).helperDocs, + }; }; diff --git a/public/app/features/transformers/rowsToFields/rowsToFields.ts b/public/app/features/transformers/rowsToFields/rowsToFields.ts index 7bdece8e98b..d7f5173273b 100644 --- a/public/app/features/transformers/rowsToFields/rowsToFields.ts +++ b/public/app/features/transformers/rowsToFields/rowsToFields.ts @@ -1,6 +1,7 @@ import { map } from 'rxjs/operators'; import { DataFrame, DataTransformerID, DataTransformerInfo, Field, getFieldDisplayName, Labels } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { EvaluatedMappingResult, @@ -16,10 +17,13 @@ export interface RowToFieldsTransformOptions { mappings?: FieldToConfigMapping[]; } -export const rowsToFieldsTransformer: DataTransformerInfo = { +export const getRowsToFieldsTransformer: () => DataTransformerInfo = () => ({ id: DataTransformerID.rowsToFields, - name: 'Rows to fields', - description: 'Convert each row into a field with dynamic config.', + name: t('transformers.get-rows-to-fields-transformer.name.rows-to-fields', 'Rows to fields'), + description: t( + 'transformers.get-rows-to-fields-transformer.description.convert-field-dynamic-config', + 'Convert each row into a field with dynamic config.' + ), defaultOptions: {}, /** @@ -32,7 +36,7 @@ export const rowsToFieldsTransformer: DataTransformerInfo rowsToFields(options, frame)); }) ), -}; +}); export function rowsToFields(options: RowToFieldsTransformOptions, data: DataFrame): DataFrame { const mappingResult = evaluateFieldMappings(data, options.mappings ?? [], true); diff --git a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx index 71c8d6d7287..dbc94ad0f0e 100644 --- a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx +++ b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx @@ -20,7 +20,7 @@ import { getTransformationContent } from '../docs/getTransformationContent'; import { SpatialCalculation, SpatialOperation, SpatialAction, SpatialTransformOptions } from './models.gen'; import { getDefaultOptions, getTransformerOptionPane } from './optionsHelper'; -import { isLineBuilderOption, spatialTransformer } from './spatialTransformer'; +import { isLineBuilderOption, getSpatialTransformer } from './spatialTransformer'; // Nothing defined in state const supplier = ( @@ -173,13 +173,16 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; -export const spatialTransformRegistryItem: TransformerRegistryItem = { - id: DataTransformerID.spatial, - editor: SetGeometryTransformerEditor, - transformation: spatialTransformer, - name: spatialTransformer.name, - description: spatialTransformer.description, - state: PluginState.alpha, - categories: new Set([TransformerCategory.PerformSpatialOperations]), - help: getTransformationContent(DataTransformerID.spatial).helperDocs, +export const getSpatialTransformRegistryItem: () => TransformerRegistryItem = () => { + const spatialTransformer = getSpatialTransformer(); + return { + id: DataTransformerID.spatial, + editor: SetGeometryTransformerEditor, + transformation: spatialTransformer, + name: spatialTransformer.name, + description: spatialTransformer.description, + state: PluginState.alpha, + categories: new Set([TransformerCategory.PerformSpatialOperations]), + help: getTransformationContent(DataTransformerID.spatial).helperDocs, + }; }; diff --git a/public/app/features/transformers/spatial/spatialTransformer.ts b/public/app/features/transformers/spatial/spatialTransformer.ts index 7bf8c2adf3f..19e0f90d50b 100644 --- a/public/app/features/transformers/spatial/spatialTransformer.ts +++ b/public/app/features/transformers/spatial/spatialTransformer.ts @@ -1,20 +1,24 @@ import { mergeMap, from } from 'rxjs'; import { DataFrame, DataTransformerID, DataTransformerInfo, FieldType } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { createGeometryCollection, createLineBetween } from 'app/features/geo/format/utils'; import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location'; import { SpatialOperation, SpatialAction, SpatialTransformOptions } from './models.gen'; import { doGeomeryCalculation, toLineString } from './utils'; -export const spatialTransformer: DataTransformerInfo = { +export const getSpatialTransformer: () => DataTransformerInfo = () => ({ id: DataTransformerID.spatial, - name: 'Spatial operations', - description: 'Apply spatial operations to query results.', + name: t('transformers.get-spatial-transformer.name.spatial-operations', 'Spatial operations'), + description: t( + 'transformers.get-spatial-transformer.description.apply-spatial-operations-to-query-results', + 'Apply spatial operations to query results.' + ), defaultOptions: {}, operator: (options) => (source) => source.pipe(mergeMap((data) => from(doSetGeometry(data, options)))), -}; +}); export function isLineBuilderOption(options: SpatialTransformOptions): boolean { return options.action === SpatialAction.Modify && options.modify?.op === SpatialOperation.LineBuilder; diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index 4f8a5598c75..3fdd629a6e4 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -1,74 +1,74 @@ import { TransformerRegistryItem } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { filterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor'; -import { heatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor'; -import { configFromQueryTransformRegistryItem } from './configFromQuery/ConfigFromQueryTransformerEditor'; -import { calculateFieldTransformRegistryItem } from './editors/CalculateFieldTransformerEditor'; -import { concatenateTransformRegistryItem } from './editors/ConcatenateTransformerEditor'; -import { convertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor'; -import { filterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor'; -import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor'; -import { formatStringTransformerRegistryItem } from './editors/FormatStringTransformerEditor'; -import { formatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor'; -import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor'; -import { groupToNestedTableTransformRegistryItem } from './editors/GroupToNestedTableTransformerEditor'; -import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor'; -import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor'; -import { joinByFieldTransformerRegistryItem } from './editors/JoinByFieldTransformerEditor'; -import { labelsToFieldsTransformerRegistryItem } from './editors/LabelsToFieldsTransformerEditor'; -import { limitTransformRegistryItem } from './editors/LimitTransformerEditor'; -import { mergeTransformerRegistryItem } from './editors/MergeTransformerEditor'; -import { organizeFieldsTransformRegistryItem } from './editors/OrganizeFieldsTransformerEditor'; -import { reduceTransformRegistryItem } from './editors/ReduceTransformerEditor'; -import { renameByRegexTransformRegistryItem } from './editors/RenameByRegexTransformer'; -import { seriesToRowsTransformerRegistryItem } from './editors/SeriesToRowsTransformerEditor'; -import { sortByTransformRegistryItem } from './editors/SortByTransformerEditor'; -import { transposeTransformerRegistryItem } from './editors/TransposeTransformerEditor'; -import { extractFieldsTransformRegistryItem } from './extractFields/ExtractFieldsTransformerEditor'; -import { joinByLabelsTransformRegistryItem } from './joinByLabels/JoinByLabelsTransformerEditor'; -import { fieldLookupTransformRegistryItem } from './lookupGazetteer/FieldLookupTransformerEditor'; -import { partitionByValuesTransformRegistryItem } from './partitionByValues/PartitionByValuesEditor'; -import { prepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor'; -import { regressionTransformerRegistryItem } from './regression/regressionEditor'; -import { rowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor'; -import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor'; -import { timeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor'; +import { getFilterByValueTransformRegistryItem } from './FilterByValueTransformer/FilterByValueTransformerEditor'; +import { getHeatmapTransformRegistryItem } from './calculateHeatmap/HeatmapTransformerEditor'; +import { getConfigFromQueryTransformRegistryItem } from './configFromQuery/ConfigFromQueryTransformerEditor'; +import { getCalculateFieldTransformRegistryItem } from './editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor'; +import { getConcatenateTransformRegistryItem } from './editors/ConcatenateTransformerEditor'; +import { getConvertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor'; +import { getFilterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor'; +import { getFilterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor'; +import { getFormatStringTransformerRegistryItem } from './editors/FormatStringTransformerEditor'; +import { getFormatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor'; +import { getGroupByTransformRegistryItem } from './editors/GroupByTransformerEditor'; +import { getGroupToNestedTableTransformRegistryItem } from './editors/GroupToNestedTableTransformerEditor'; +import { getGroupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor'; +import { getHistogramTransformRegistryItem } from './editors/HistogramTransformerEditor'; +import { getJoinByFieldTransformerRegistryItem } from './editors/JoinByFieldTransformerEditor'; +import { getLabelsToFieldsTransformerRegistryItem } from './editors/LabelsToFieldsTransformerEditor'; +import { getLimitTransformRegistryItem } from './editors/LimitTransformerEditor'; +import { getMergeTransformerRegistryItem } from './editors/MergeTransformerEditor'; +import { getOrganizeFieldsTransformRegistryItem } from './editors/OrganizeFieldsTransformerEditor'; +import { getReduceTransformRegistryItem } from './editors/ReduceTransformerEditor'; +import { getRenameByRegexTransformRegistryItem } from './editors/RenameByRegexTransformer'; +import { getSeriesToRowsTransformerRegistryItem } from './editors/SeriesToRowsTransformerEditor'; +import { getSortByTransformRegistryItem } from './editors/SortByTransformerEditor'; +import { getTransposeTransformerRegistryItem } from './editors/TransposeTransformerEditor'; +import { getExtractFieldsTransformRegistryItem } from './extractFields/ExtractFieldsTransformerEditor'; +import { getJoinByLabelsTransformRegistryItem } from './joinByLabels/JoinByLabelsTransformerEditor'; +import { getFieldLookupTransformRegistryItem } from './lookupGazetteer/FieldLookupTransformerEditor'; +import { getPartitionByValuesTransformRegistryItem } from './partitionByValues/PartitionByValuesEditor'; +import { getPrepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor'; +import { getRegressionTransformerRegistryItem } from './regression/regressionEditor'; +import { getRowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor'; +import { getSpatialTransformRegistryItem } from './spatial/SpatialTransformerEditor'; +import { getTimeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor'; export const getStandardTransformers = (): TransformerRegistryItem[] => { return [ - reduceTransformRegistryItem, - filterFieldsByNameTransformRegistryItem, - renameByRegexTransformRegistryItem, - filterFramesByRefIdTransformRegistryItem, - filterByValueTransformRegistryItem, - organizeFieldsTransformRegistryItem, - joinByFieldTransformerRegistryItem, - seriesToRowsTransformerRegistryItem, - concatenateTransformRegistryItem, - calculateFieldTransformRegistryItem, - labelsToFieldsTransformerRegistryItem, - groupByTransformRegistryItem, - sortByTransformRegistryItem, - mergeTransformerRegistryItem, - histogramTransformRegistryItem, - rowsToFieldsTransformRegistryItem, - configFromQueryTransformRegistryItem, - prepareTimeseriesTransformerRegistryItem, - convertFieldTypeTransformRegistryItem, - spatialTransformRegistryItem, - fieldLookupTransformRegistryItem, - extractFieldsTransformRegistryItem, - heatmapTransformRegistryItem, - groupingToMatrixTransformRegistryItem, - limitTransformRegistryItem, - joinByLabelsTransformRegistryItem, - regressionTransformerRegistryItem, - partitionByValuesTransformRegistryItem, - ...(config.featureToggles.formatString ? [formatStringTransformerRegistryItem] : []), - ...(config.featureToggles.groupToNestedTableTransformation ? [groupToNestedTableTransformRegistryItem] : []), - formatTimeTransformerRegistryItem, - timeSeriesTableTransformRegistryItem, - transposeTransformerRegistryItem, + getReduceTransformRegistryItem(), + getFilterFieldsByNameTransformRegistryItem(), + getRenameByRegexTransformRegistryItem(), + getFilterFramesByRefIdTransformRegistryItem(), + getFilterByValueTransformRegistryItem(), + getOrganizeFieldsTransformRegistryItem(), + getJoinByFieldTransformerRegistryItem(), + getSeriesToRowsTransformerRegistryItem(), + getConcatenateTransformRegistryItem(), + getCalculateFieldTransformRegistryItem(), + getLabelsToFieldsTransformerRegistryItem(), + getGroupByTransformRegistryItem(), + getSortByTransformRegistryItem(), + getMergeTransformerRegistryItem(), + getHistogramTransformRegistryItem(), + getRowsToFieldsTransformRegistryItem(), + getConfigFromQueryTransformRegistryItem(), + getPrepareTimeseriesTransformerRegistryItem(), + getConvertFieldTypeTransformRegistryItem(), + getSpatialTransformRegistryItem(), + getFieldLookupTransformRegistryItem(), + getExtractFieldsTransformRegistryItem(), + getHeatmapTransformRegistryItem(), + getGroupingToMatrixTransformRegistryItem(), + getLimitTransformRegistryItem(), + getJoinByLabelsTransformRegistryItem(), + getRegressionTransformerRegistryItem(), + getPartitionByValuesTransformRegistryItem(), + ...(config.featureToggles.formatString ? [getFormatStringTransformerRegistryItem()] : []), + ...(config.featureToggles.groupToNestedTableTransformation ? [getGroupToNestedTableTransformRegistryItem()] : []), + getFormatTimeTransformerRegistryItem(), + getTimeSeriesTableTransformRegistryItem(), + getTransposeTransformerRegistryItem(), ]; }; diff --git a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx index bc47f4eb04c..d90702f5f35 100644 --- a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx +++ b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx @@ -17,7 +17,7 @@ import { InlineFieldRow, InlineField, StatsPicker, Select, InlineLabel } from '@ import { getTransformationContent } from '../docs/getTransformationContent'; import { - timeSeriesTableTransformer, + getTimeSeriesTableTransformer, TimeSeriesTableTransformerOptions, getRefData, } from './timeSeriesTableTransformer'; @@ -123,12 +123,16 @@ export function TimeSeriesTableTransformEditor({ return <>{configRows}; } -export const timeSeriesTableTransformRegistryItem: TransformerRegistryItem = { - id: timeSeriesTableTransformer.id, - editor: TimeSeriesTableTransformEditor, - transformation: timeSeriesTableTransformer, - name: timeSeriesTableTransformer.name, - description: timeSeriesTableTransformer.description, - state: PluginState.beta, - help: getTransformationContent(timeSeriesTableTransformer.id).helperDocs, -}; +export const getTimeSeriesTableTransformRegistryItem: () => TransformerRegistryItem = + () => { + const timeSeriesTableTransformer = getTimeSeriesTableTransformer(); + return { + id: timeSeriesTableTransformer.id, + editor: TimeSeriesTableTransformEditor, + transformation: timeSeriesTableTransformer, + name: timeSeriesTableTransformer.name, + description: timeSeriesTableTransformer.description, + state: PluginState.beta, + help: getTransformationContent(timeSeriesTableTransformer.id).helperDocs, + }; + }; diff --git a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts index fc20a059fde..d3e4073022b 100644 --- a/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts +++ b/public/app/features/transformers/timeSeriesTable/timeSeriesTableTransformer.ts @@ -14,6 +14,7 @@ import { TransformationApplicabilityLevels, isTimeSeriesField, } from '@grafana/data'; +import { t } from '@grafana/i18n'; /** * Maps a refId to a Field which can contain @@ -81,10 +82,13 @@ export interface RefIdTransformerOptions { inlineStat?: boolean; } -export const timeSeriesTableTransformer: DataTransformerInfo = { +export const getTimeSeriesTableTransformer: () => DataTransformerInfo = () => ({ id: DataTransformerID.timeSeriesTable, - name: 'Time series to table', - description: 'Convert time series data to table rows so that they can be viewed in tabular or sparkline format.', + name: t('transformers.time-series-table.name.time-series-to-table', 'Time series to table'), + description: t( + 'transformers.time-series-table.description.convert-to-table-rows', + 'Convert time series data to table rows so that they can be viewed in tabular or sparkline format.' + ), defaultOptions: {}, isApplicable: (data) => { for (const frame of data) { @@ -95,15 +99,17 @@ export const timeSeriesTableTransformer: DataTransformerInfo (source) => source.pipe( map((data) => { return timeSeriesToTableTransform(options, data); }) ), -}; +}); /** * Converts time series frames to table frames for use with sparkline chart type. diff --git a/public/app/features/transformers/utils.ts b/public/app/features/transformers/utils.ts index fcb18493e90..7cbf4cd1241 100644 --- a/public/app/features/transformers/utils.ts +++ b/public/app/features/transformers/utils.ts @@ -62,15 +62,21 @@ export function getDistinctLabels(input: DataFrame[]): Set { return distinct; } -export const categoriesLabels: { [K in TransformerCategory]: string } = { - combine: 'Combine', - calculateNewFields: 'Calculate new fields', - createNewVisualization: 'Create new visualization', - filter: 'Filter', - performSpatialOperations: 'Perform spatial operations', - reformat: 'Reformat', - reorderAndRename: 'Reorder and rename', -}; +export const getCategoriesLabels: () => { [K in TransformerCategory]: string } = () => ({ + combine: t('transformers.utils.get-categories-labels.combine', 'Combine'), + calculateNewFields: t('transformers.utils.get-categories-labels.calculate-new-fields', 'Calculate new fields'), + createNewVisualization: t( + 'transformers.utils.get-categories-labels.create-new-visualization', + 'Create new visualization' + ), + filter: t('transformers.utils.get-categories-labels.filter', 'Filter'), + performSpatialOperations: t( + 'transformers.utils.get-categories-labels.perform-spatial-operations', + 'Perform spatial operations' + ), + reformat: t('transformers.utils.get-categories-labels.reformat', 'Reformat'), + reorderAndRename: t('transformers.utils.get-categories-labels.reorder-and-rename', 'Reorder and rename'), +}); export const numberOrVariableValidator = (value: string | number) => { if (typeof value === 'number') { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 6daba435bae..ea3d5595fbb 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5259,7 +5259,8 @@ "transformation-picker-ng": { "placeholder-search-for-transformation": "Search for transformation", "show-images": "Show images", - "title-add-another-transformation": "Add another transformation" + "title-add-another-transformation": "Add another transformation", + "view-all": "View all" }, "un-theme-transformations-editor": { "body-delete-all-transformations": "By deleting all transformations, you will go back to the main selection screen." @@ -12230,6 +12231,18 @@ "placeholder-fields-or-number": "Field(s) or number" }, "calculate-field-transformer-editor": { + "calculation-modes": { + "label": { + "binary-operation": "Binary operation", + "reduce-row": "Reduce row", + "row-index": "Row index", + "unary-operation": "Unary operation" + } + }, + "label": { + "cumulative-functions": "Cumulative functions", + "window-functions": "Window functions" + }, "label-alias": "Alias", "label-mode": "Mode", "label-replace-all-fields": "Replace all fields" @@ -12244,6 +12257,13 @@ "concatenate-transformer-editor": { "label-label": "Label", "label-name": "Name", + "name-modes": { + "label": { + "copy-frame-name-to-field": "Copy frame name to field name", + "ignore-the-frame-name": "Ignore the frame name", + "label-frame": "Add a label with the frame name" + } + }, "placeholder-frame": "Frame" }, "config-from-query-transformer-editor": { @@ -12255,6 +12275,9 @@ "aria-label-add-a-convert-field-type-transformer": "Add a convert field type transformer", "aria-label-remove-convert-field-type-transformer": "Remove convert field type transformer", "convert-field-type": "Convert field type", + "description": { + "convert-to-specified-field-type": "Convert a field to a specified field type." + }, "label": { "browser": "Browser", "utc": "UTC" @@ -12265,7 +12288,13 @@ "label-input-format": "Input format", "label-join-with": "Join with", "label-set-timezone": "Set timezone", + "name": { + "convert-field-type": "Convert field type" + }, "placeholder-type": "Type", + "tags": { + "format-field": "Format field" + }, "tooltip-explicit-separator-joining-array-values": "Use an explicit separator when joining array values", "tooltip-input-format": "Specify the format of the input field so Grafana can parse the date string correctly.", "tooltip-specify-the-output-format": "Specify the output format.", @@ -12287,6 +12316,19 @@ "desc": "DESC", "off": "Off" }, + "editors": { + "concatenate-transformer-editor": { + "description": { + "combine-all-fields": "Combine all fields into a single frame." + }, + "name": { + "concatenate-fields": "Concatenate fields" + }, + "tags": { + "combine": "Combine" + } + } + }, "enum-mapping-editor": { "add-enum-value": "Add enum value", "generate-enum-values-from-data": "Generate enum values from data" @@ -12297,6 +12339,9 @@ "remove-enum-row-tooltip-delete": "Delete" }, "extract-fields-transformer-editor": { + "description": { + "parse-fields-from-content": "Parse fields from content (JSON, labels, etc)." + }, "field-name-picker-settings": { "placeholderText": { "select-field": "Select field" @@ -12307,6 +12352,9 @@ "label-keep-time": "Keep time", "label-replace-all-fields": "Replace all fields", "label-source": "Source", + "name": { + "extract-fields": "Extract fields" + }, "placeholder-auto": "Auto", "placeholder-select-delimiter": "Select delimiter...", "tooltip-regexp": "Example: {{regexExample}}" @@ -12315,6 +12363,9 @@ "aria-label-threshold-color": "Threshold color" }, "field-lookup-transformer-editor": { + "description": { + "lookup-additional-fields-external-source": "Use a field value to lookup countries, states, or airports." + }, "field-name-picker-settings": { "noFieldsMessage": { "no-text-fields-found": "No text fields found" @@ -12324,7 +12375,10 @@ } }, "label-field": "Field", - "label-lookup": "Lookup" + "label-lookup": "Lookup", + "name": { + "lookup-fields-from-resource": "Lookup fields from resource" + } }, "field-to-config-mapping-editor": { "additional-settings": "Additional settings", @@ -12333,12 +12387,26 @@ "use-as": "Use as" }, "filter-by-name-transformer-editor": { + "description": { + "remove-part-query-results-regex-pattern": "Remove parts of the query results using a regex pattern." + }, "label-from-variable": "From variable", "label-identifier": "Identifier", "label-use-variable": "Use variable", "label-variable": "Variable", + "name": { + "filter-fields-by-name": "Filter fields by name" + }, "placeholder-regular-expression-pattern": "Regular expression pattern" }, + "filter-by-ref-id-transformer-editor": { + "description": { + "filter-data-by-query-useful-sharing-results": "Remove rows from the data based on origin query" + }, + "name": { + "filter-data-by-query": "Filter data by query" + } + }, "filter-by-value-filter-editor": { "label-field": "Field", "label-match": "Match", @@ -12348,6 +12416,9 @@ }, "filter-by-value-transformer-editor": { "add-condition": "Add condition", + "description": { + "remove-rows-query-results-user-defined-filters": "Remove rows from the query results using user-defined filters." + }, "filter-match": { "label": { "match-all": "Match all", @@ -12361,7 +12432,10 @@ } }, "label-conditions": "Conditions", - "label-filter-type": "Filter type" + "label-filter-type": "Filter type", + "name": { + "filter-data-by-values": "Filter data by values" + } }, "format-string-transfomer-editor": { "field-name-picker-settings": { @@ -12376,6 +12450,14 @@ "label-format": "Format", "label-substring-range": "Substring range" }, + "format-string-transformer-editor": { + "description": { + "manipulate-string-fields-formatting": "Manipulate string fields formatting." + }, + "name": { + "format-string": "Format string" + } + }, "format-time-transfomer-editor": { "label-format": "Format", "label-set-timezone": "Set timezone", @@ -12383,6 +12465,70 @@ "tooltip-format": "The output format for the field specified as a <2>Moment.js format string.", "tooltip-timezone-manually": "Set the timezone of the date manually" }, + "format-time-transformer-editor": { + "description": { + "set-based-on-time": "Set the output format of a time field" + }, + "name": { + "format-time": "Format time" + } + }, + "get-calculate-field-transform-registry-item": { + "description": { + "values-calculate-field": "Use the row values to calculate a new field." + }, + "name": { + "add-field-from-calculation": "Add field from calculation" + } + }, + "get-config-from-data-transformer": { + "description": { + "set-unit-min-max-and-more": "Set unit, min, max and more." + }, + "name": { + "config-from-query-results": "Config from query results" + } + }, + "get-heatmap-transformer": { + "description": { + "generate-heatmap-data-from-source": "Generate heatmap data from source data." + }, + "name": { + "create-heatmap": "Create heatmap" + } + }, + "get-join-by-labels-transformer": { + "description": { + "flatten-labeled-results-table-joined-labels": "Flatten labeled results into a table joined by labels." + }, + "name": { + "join-by-labels": "Join by labels" + } + }, + "get-partition-by-values-transformer": { + "description": { + "split-oneframe-dataset-multiple-series": "Split a one-frame dataset into multiple series." + }, + "name": { + "partition-by-values": "Partition by values" + } + }, + "get-rows-to-fields-transformer": { + "description": { + "convert-field-dynamic-config": "Convert each row into a field with dynamic config." + }, + "name": { + "rows-to-fields": "Rows to fields" + } + }, + "get-spatial-transformer": { + "description": { + "apply-spatial-operations-to-query-results": "Apply spatial operations to query results." + }, + "name": { + "spatial-operations": "Spatial operations" + } + }, "get-timezone-options": { "label": { "browser": "Browser", @@ -12410,22 +12556,46 @@ } }, "group-by-field-configuration": { + "options": { + "label": { + "calculate": "Calculate", + "group-by": "Group by" + } + }, "placeholder-ignored": "Ignored", "placeholder-select-stats": "Select stats" }, "group-by-transformer-editor": { + "description": { + "group-series-by-field-calculate-stats": "Group data by a field value and create aggregate data." + }, + "name": { + "group-by": "Group by" + }, "title-calc-alert": "Calculations will not have an effect if no fields are being grouped on" }, "group-to-nested-table-transformer-editor": { + "description": { + "group-by-field-value": "Group data by a field value and create nested tables with the grouped data." + }, "description-show-field-names": "If enabled nested tables will show field names as a table header", "label-show-field-names-in-nested-tables": "Show field names in nested tables", + "name": { + "group-to-nested-tables": "Group to nested tables" + }, "title-calc-alert": "Calculations will not have an effect if no fields are being grouped on." }, "grouping-to-matrix-transformer-editor": { + "description": { + "summarize-and-reorganize-data": "Summarize and reorganize data based on three fields." + }, "label-cell-value": "Cell value", "label-column": "Column", "label-empty-value": "Empty value", "label-row": "Row", + "name": { + "grouping-to-matrix": "Grouping to matrix" + }, "special-value-options": { "description": { "boolean-false-value": "Boolean false value", @@ -12444,6 +12614,12 @@ } }, "histogram-transformer-editor": { + "description": { + "calculate-histogram-from-input-data": "Calculate a histogram from input data." + }, + "name": { + "histogram": "Histogram" + }, "placeholder-auto": "Auto", "placeholder-default": "Default: 30", "placeholder-none": "None" @@ -12452,7 +12628,16 @@ "label-as-percentile": "As percentile", "tooltip-transform-index-percentile": "Transform the row index as a percentile." }, + "join-by-field-transformer-editor": { + "description": { + "combine-rows-from-2-tables": "Combine rows from 2+ tables, based on a related field." + }, + "name": { + "join-by-field": "Join by field" + } + }, "join-by-labels-transformer-editor": { + "error-required": "Required", "info": { "input-contain-labels": "The input does not contain any labels", "no-input-or-labels-found": "No input (or labels) found", @@ -12474,10 +12659,30 @@ "label-labels": "Labels", "label-mode": "Mode", "label-value-field-name": "Value field name", + "modes": { + "label": { + "columns": "Columns", + "rows": "Rows" + } + }, "placeholder-optional-select-label": "(Optional) Select label", "tooltip-replace-value-field-label": "Replace the value field name with a label" }, + "labels-to-fields-transformer-editor": { + "description": { + "groups-series-time-return-labels-tags-fields": "Group series by time and return labels or tags as fields." + }, + "name": { + "labels-to-fields": "Labels to fields" + } + }, "limit-transformer-editor": { + "description": { + "limit-number-items-displayed": "Limit the number of items displayed." + }, + "name": { + "limit": "Limit" + }, "placeholder-value-or-variable": "Value or variable" }, "long-info": { @@ -12488,7 +12693,13 @@ "string-values-separate": "String values are represented as separate fields rather than as labels" }, "merge-transformer-editor": { - "merge-effect-applied-single-frame": "Merge has no effect when applied on a single frame." + "description": { + "merge-multiple-series": "Merge multiple series. Values will be combined into one row." + }, + "merge-effect-applied-single-frame": "Merge has no effect when applied on a single frame.", + "name": { + "merge": "Merge series/tables" + } }, "multi-info": { "all-values-are-numeric": "All values are numeric", @@ -12498,10 +12709,16 @@ "time-in-ascending-order": "Time in ascending order" }, "organize-fields-transformer-editor": { + "description": { + "reorder-hide-or-rename-fields": "Re-order, hide, or rename fields." + }, "field-order": "Field order", "field-order-auto": "Auto", "field-order-manual": "Manual", - "first-frame-warning": "Organize fields only works with a single frame. Consider applying a join transformation or filtering the input first." + "first-frame-warning": "Organize fields only works with a single frame. Consider applying a join transformation or filtering the input first.", + "name": { + "organize-fields": "Organize fields by name" + } }, "partion-by-values-editor": { "tooltip-naming": "Sets how the names of the selected fields are displayed. As frame name is usually better for tabular data" @@ -12526,9 +12743,41 @@ "partition-values-works-single-frame": "Partition by values only works with a single frame.", "tooltip-keeps-partition-fields-frames": "Keeps the partition fields in the frames" }, + "prepare-time-series": { + "description": { + "stretch-data-frames": "Stretch data frames from the wide format into the long format." + }, + "name": { + "prepare-time-series": "Prepare time series" + } + }, "prepare-time-series-editor": { "label-format": "Format", - "label-info": "Info" + "label-info": "Info", + "long-info": { + "description": { + "convert-each-frame-to-long-format": "Convert each frame to long format" + }, + "label": { + "long-time-series": "Long time series" + } + }, + "multi-info": { + "description": { + "creates-frame-timenumber": "Creates a new frame for each time/number pair" + }, + "label": { + "multiframe-time-series": "Multi-frame time series" + } + }, + "wide-info": { + "description": { + "creates-single-frame-joined": "Creates a single frame joined by time" + }, + "label": { + "wide-time-series": "Wide time series" + } + } }, "range-matcher-editor": { "and": "and", @@ -12540,6 +12789,9 @@ "label-operation": "Operation" }, "reduce-transformer-editor": { + "description": { + "reduce-to-single-value": "Reduce all rows or data points to a single value (ex. max, mean)." + }, "label-calculations": "Calculations", "label-include-time": "Include time", "label-labels-to-fields": "Labels to fields", @@ -12554,12 +12806,23 @@ "series-to-rows": "Series to rows" } }, + "name": { + "reduce": "Reduce" + }, "placeholder-choose-stat": "Choose stat" }, "regex-matcher-editor": { "placeholder-value-or-variable": "Value or variable", "variable-regex-documentation": "Formats multi-value variable into a regex string" }, + "regression": { + "description": { + "create-new-data-frame": "Create a new data frame containing values predicted by a statistical model." + }, + "name": { + "regression-analysis": "Regression analysis" + } + }, "regression-transformer-editor": { "label": { "cubic": "Cubic", @@ -12581,6 +12844,14 @@ "regression": "regression", "tooltip-number-of-xy-points-to-predict": "Number of X,Y points to predict" }, + "rename-by-regex-transformer": { + "description": { + "rename-parts-using-regex": "Rename parts of the query results using a regular expression and replacement pattern." + }, + "name": { + "rename-fields-by-regex": "Rename fields by regex" + } + }, "rename-by-regex-transformer-editor": { "label-match": "Match", "label-replace": "Replace", @@ -12589,11 +12860,37 @@ }, "series-to-fields-transformer-editor": { "label-field": "Field", - "label-mode": "Mode" + "label-mode": "Mode", + "modes": { + "description": { + "combine-data-from-two-tables": "Combine data from two tables whenever there are matching values in a fields common to both tables.", + "join-on-a-field": "Join on a field value with duplicated values. Non performant outer join best used for tabular(SQL like) data.", + "keep-all-rows": "Keep all rows from any table with a value. Join on distinct field values. Performant and best used for time series." + }, + "label": { + "inner": "Inner", + "outer-tabular": "Outer (tabular)", + "outer-time-series": "Outer (time series)" + } + } + }, + "series-to-rows-transformer-editor": { + "description": { + "merge-multiple-series": "Merge multiple series. Return time, metric and values as a row." + }, + "name": { + "series-to-rows": "Series to rows" + } }, "sort-by-transformer-editor": { + "description": { + "sort-fields": "Sort fields in a frame." + }, "label-field": "Field", "label-reverse": "Reverse", + "name": { + "sort-by": "Sort by" + }, "placeholder-select-field": "Select field" }, "supplier": { @@ -12613,7 +12910,17 @@ "transform": "Transform" } }, - "tag": "{{ tag }}", + "time-series-table": { + "description": { + "convert-to-table-rows": "Convert time series data to table rows so that they can be viewed in tabular or sparkline format." + }, + "is-applicable-description": { + "requires-time-series-frame": "The Time series to table transformation requires at least one time series frame to function. You currently have none." + }, + "name": { + "time-series-to-table": "Time series to table" + } + }, "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Time field", @@ -12627,10 +12934,34 @@ "placeholder-value": "Value", "tooltip-name-for-value-fields": "Name for value fields" }, + "transpose-transformer-editor": { + "description": { + "transpose-data-frame": "Transpose the data frame." + }, + "name": { + "transpose": "Transpose" + }, + "tags": { + "pivot": "Pivot", + "transform": "Transform", + "translate": "Translate" + } + }, "unary-operation-editor": { "label-operation": "Operation", "placeholder-field": "Field" }, + "utils": { + "get-categories-labels": { + "calculate-new-fields": "Calculate new fields", + "combine": "Combine", + "create-new-visualization": "Create new visualization", + "filter": "Filter", + "perform-spatial-operations": "Perform spatial operations", + "reformat": "Reformat", + "reorder-and-rename": "Reorder and rename" + } + }, "wide-info": { "multiple-value-fields-of-any-type": "Multiple value fields of any type", "single-frame": "Single frame", From d6f52d5d9be2b39b18227e9c7393f2dab2c8574d Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:16:28 +0000 Subject: [PATCH 11/25] I18n: Download translations from Crowdin (#107998) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 3 ++- public/locales/de-DE/grafana.json | 3 ++- public/locales/es-ES/grafana.json | 3 ++- public/locales/fr-FR/grafana.json | 3 ++- public/locales/hu-HU/grafana.json | 3 ++- public/locales/id-ID/grafana.json | 3 ++- public/locales/it-IT/grafana.json | 3 ++- public/locales/ja-JP/grafana.json | 3 ++- public/locales/ko-KR/grafana.json | 3 ++- public/locales/nl-NL/grafana.json | 3 ++- public/locales/pl-PL/grafana.json | 3 ++- public/locales/pt-BR/grafana.json | 3 ++- public/locales/pt-PT/grafana.json | 3 ++- public/locales/ru-RU/grafana.json | 3 ++- public/locales/sv-SE/grafana.json | 3 ++- public/locales/tr-TR/grafana.json | 3 ++- public/locales/zh-Hans/grafana.json | 3 ++- public/locales/zh-Hant/grafana.json | 3 ++- 18 files changed, 36 insertions(+), 18 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 913c87e01d0..a09174c76d3 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -3586,11 +3586,11 @@ "button-create": "Vytvořit", "button-creating": "Vytváření…", "cancel": "Zrušit", - "error-invalid-folder-name": "Neplatný název složky", "error-required": "Název složky je povinný", "folder-name-input-placeholder-enter-folder-name": "Zadejte název složky", "label-folder-name": "Název složky", "text-pull-request-created": "Byl vytvořen pull request se změnami v této složce:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull request byl vytvořen", "title-this-repository-is-read-only": "Toto úložiště je jen pro čtení" }, @@ -12627,6 +12627,7 @@ "transform": "Transformovat" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Statistika", "label-time-field": "Pole času", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index d55bb859eda..bcad4385071 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Erstellen", "button-creating": "Wird erstellt ...", "cancel": "Abbrechen", - "error-invalid-folder-name": "Ungültiger Ordnername", "error-required": "Ordnername erforderlich", "folder-name-input-placeholder-enter-folder-name": "Ordnernamen eingeben", "label-folder-name": "Ordnername", "text-pull-request-created": "Ein Pull-Request wurde erstellt, mit Änderungen an diesem Ordner:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull-Request erstellt", "title-this-repository-is-read-only": "Dieses Repository ist schreibgeschützt" }, @@ -12549,6 +12549,7 @@ "transform": "Transformieren" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Stat.", "label-time-field": "Zeitfeld", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index fb3b395d998..f6a4d96590f 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Crear", "button-creating": "Creando...", "cancel": "Cancelar", - "error-invalid-folder-name": "Nombre de carpeta no válido", "error-required": "Nombre de la carpeta obligatorio", "folder-name-input-placeholder-enter-folder-name": "Introducir el nombre de la carpeta", "label-folder-name": "Nombre de la carpeta", "text-pull-request-created": "Se ha creado una solicitud de extracción con cambios en esta carpeta:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Solicitud de extracción creada", "title-this-repository-is-read-only": "Este repositorio es de solo lectura" }, @@ -12549,6 +12549,7 @@ "transform": "Transformar" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Estadística", "label-time-field": "Campo de tiempo", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 46c4e18b668..17dbe8e336d 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Créer", "button-creating": "Création...", "cancel": "Annuler", - "error-invalid-folder-name": "Nom de dossier non valide", "error-required": "Un nom de dossier est obligatoire", "folder-name-input-placeholder-enter-folder-name": "Saisir le nom du dossier", "label-folder-name": "Nom du dossier", "text-pull-request-created": "Une demande de fusion a été créée avec des modifications dans ce dossier :", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Demande de fusion créée", "title-this-repository-is-read-only": "Ce référentiel est en lecture seule" }, @@ -12549,6 +12549,7 @@ "transform": "Transformer" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Champ horaire", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index ffac048a310..c17054f7111 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Létrehozás", "button-creating": "Létrehozás…", "cancel": "Mégse", - "error-invalid-folder-name": "Érvénytelen mappanév", "error-required": "A mappa nevének megadása kötelező", "folder-name-input-placeholder-enter-folder-name": "Mappanév megadása", "label-folder-name": "Mappa neve", "text-pull-request-created": "A lekérési kérelem ennek a mappának a módosításával lett létrehozva:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Lekérési kérelem létrehozva", "title-this-repository-is-read-only": "Ez az adattár csak olvasható" }, @@ -12549,6 +12549,7 @@ "transform": "Átalakítás" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Statisztika", "label-time-field": "Időmező", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 4ff84a3c438..da4f3d69992 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -3532,11 +3532,11 @@ "button-create": "Buat", "button-creating": "Membuat...", "cancel": "Batalkan", - "error-invalid-folder-name": "Nama folder tidak valid", "error-required": "Nama folder wajib diisi", "folder-name-input-placeholder-enter-folder-name": "Masukkan nama folder", "label-folder-name": "Nama folder", "text-pull-request-created": "Permintaan pull telah dibuat dengan perubahan pada folder ini:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Permintaan pull dibuat", "title-this-repository-is-read-only": "Repositori ini hanya dapat dibaca" }, @@ -12510,6 +12510,7 @@ "transform": "Ubah" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Bidang waktu", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index a336dbddb1e..600615094cd 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Crea", "button-creating": "Creazione in corso...", "cancel": "Annulla", - "error-invalid-folder-name": "Nome cartella non valido", "error-required": "Il nome della cartella è obbligatorio", "folder-name-input-placeholder-enter-folder-name": "Inserisci nome cartella", "label-folder-name": "Nome cartella", "text-pull-request-created": "È stata creata una richiesta di pull con modifiche a questa cartella:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Richiesta di pull creata", "title-this-repository-is-read-only": "Questo repository è di sola lettura" }, @@ -12549,6 +12549,7 @@ "transform": "" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Stat.", "label-time-field": "Campo dell'ora", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 8bbb86a8c04..c188097d135 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -3532,11 +3532,11 @@ "button-create": "作成", "button-creating": "作成中...", "cancel": "キャンセル", - "error-invalid-folder-name": "無効なフォルダ名", "error-required": "フォルダ名は必須です", "folder-name-input-placeholder-enter-folder-name": "フォルダ名を入力してください", "label-folder-name": "フォルダ名", "text-pull-request-created": "このフォルダへの変更を含むプルリクエストが作成されました。", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "プルリクエストが作成されました", "title-this-repository-is-read-only": "このリポジトリは読み取り専用です" }, @@ -12510,6 +12510,7 @@ "transform": "変換" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "統計", "label-time-field": "時間フィールド", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 79c29d444c2..c8bd18b12ac 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -3532,11 +3532,11 @@ "button-create": "생성", "button-creating": "생성 중...", "cancel": "취소", - "error-invalid-folder-name": "유효하지 않은 폴더 이름", "error-required": "폴더 이름은 필수 입력 항목입니다", "folder-name-input-placeholder-enter-folder-name": "폴더 이름 입력", "label-folder-name": "폴더 이름", "text-pull-request-created": "이 폴더에 대한 변경 사항을 포함하는 풀 요청이 생성되었습니다.", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "풀 요청 생성됨", "title-this-repository-is-read-only": "이 리포지토리는 읽기 전용입니다." }, @@ -12510,6 +12510,7 @@ "transform": "변환" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "통계", "label-time-field": "시간 필드", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index fcdb655b5d6..657c0db9c1c 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Aanmaken", "button-creating": "Aanmaken...", "cancel": "Annuleren", - "error-invalid-folder-name": "Ongeldige mapnaam", "error-required": "Mapnaam is vereist", "folder-name-input-placeholder-enter-folder-name": "Mapnaam invoeren", "label-folder-name": "Mapnaam", "text-pull-request-created": "Er is een pull request gemaakt met wijzigingen in deze map:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull request is aangemaakt", "title-this-repository-is-read-only": "Deze repository is alleen-lezen" }, @@ -12549,6 +12549,7 @@ "transform": "Transformeren" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Tijdveld", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index f3936162e51..36ca7c77482 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -3586,11 +3586,11 @@ "button-create": "Utwórz", "button-creating": "Tworzenie…", "cancel": "Anuluj", - "error-invalid-folder-name": "Nieprawidłowa nazwa folderu", "error-required": "Wymagana jest nazwa folderu", "folder-name-input-placeholder-enter-folder-name": "Podaj nazwę folderu", "label-folder-name": "Nazwa folderu", "text-pull-request-created": "Utworzono żądanie pull ze zmianami w tym folderze:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Utworzono żądanie pull", "title-this-repository-is-read-only": "To repozytorium jest tylko do odczytu" }, @@ -12627,6 +12627,7 @@ "transform": "Przekształć" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Statystyka", "label-time-field": "Pole czasu", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index af641b53d45..30857e8e4c3 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Criar", "button-creating": "Criando…", "cancel": "Cancelar", - "error-invalid-folder-name": "Nome da pasta inválido", "error-required": "O nome da pasta é obrigatório", "folder-name-input-placeholder-enter-folder-name": "Digitar o nome da pasta", "label-folder-name": "Nome da pasta", "text-pull-request-created": "Uma solicitação de extração foi criada com alterações nesta pasta:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Solicitação de extração criada", "title-this-repository-is-read-only": "Este repositório é somente leitura" }, @@ -12549,6 +12549,7 @@ "transform": "Transformar" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Estatística", "label-time-field": "Campo de hora", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index dee16762e41..22f0c4e6d53 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Criar", "button-creating": "A criar...", "cancel": "Cancelar", - "error-invalid-folder-name": "Nome da pasta inválido", "error-required": "O nome da pasta é obrigatório", "folder-name-input-placeholder-enter-folder-name": "Introduza o nome da pasta", "label-folder-name": "Nome da pasta", "text-pull-request-created": "Foi criado um pedido de extração com alterações a esta pasta:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pedido de extração criado", "title-this-repository-is-read-only": "Este repositório é apenas de leitura" }, @@ -12549,6 +12549,7 @@ "transform": "Transformar" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Estatística", "label-time-field": "Campo de hora", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index f84ae7d9c8b..8541b4c46d5 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -3586,11 +3586,11 @@ "button-create": "Создание", "button-creating": "Создание...", "cancel": "Отмена", - "error-invalid-folder-name": "Недопустимое имя папки", "error-required": "Требуется имя папки", "folder-name-input-placeholder-enter-folder-name": "Ввести имя папки", "label-folder-name": "Имя папки", "text-pull-request-created": "Создан запрос на включение изменений в папке:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Создан запрос на включение изменений", "title-this-repository-is-read-only": "Репозиторий предназначен только для чтения" }, @@ -12627,6 +12627,7 @@ "transform": "Преобразовать" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Статистика", "label-time-field": "Поле времени", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 3d81843fb04..e72a12d3d7d 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Skapa", "button-creating": "Skapar …", "cancel": "Avbryt", - "error-invalid-folder-name": "Ogiltigt mappnamn", "error-required": "Mappnamn krävs", "folder-name-input-placeholder-enter-folder-name": "Ange mappnamn", "label-folder-name": "Mappnamn", "text-pull-request-created": "En pull-begäran har skapats med ändringar i den här mappen:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Pull-begäran skapad", "title-this-repository-is-read-only": "Den här databasen är skrivskyddad" }, @@ -12549,6 +12549,7 @@ "transform": "Omvandla" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "Stat", "label-time-field": "Tidsfält", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 262db9aa789..11ede563210 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -3550,11 +3550,11 @@ "button-create": "Oluştur", "button-creating": "Oluşturuluyor...", "cancel": "İptal", - "error-invalid-folder-name": "Geçersiz klasör adı", "error-required": "Klasör adı gereklidir", "folder-name-input-placeholder-enter-folder-name": "Klasör adını girin", "label-folder-name": "Klasör adı", "text-pull-request-created": "Bu klasörde yapılan değişikliklerle bir çekme isteği oluşturuldu:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "Çekme isteği oluşturuldu", "title-this-repository-is-read-only": "Bu depo salt okunur" }, @@ -12549,6 +12549,7 @@ "transform": "Dönüştür" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "İstatistik", "label-time-field": "Zaman alanı", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index bfaef6559b7..f02911d6b2d 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -3532,11 +3532,11 @@ "button-create": "创建", "button-creating": "正在创建...", "cancel": "取消", - "error-invalid-folder-name": "文件夹名称无效", "error-required": "文件夹名称为必填项", "folder-name-input-placeholder-enter-folder-name": "输入文件夹名称", "label-folder-name": "文件夹名称", "text-pull-request-created": "已创建拉取请求,对此文件夹进行更改:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "拉取请求已创建", "title-this-repository-is-read-only": "此存储库为只读" }, @@ -12510,6 +12510,7 @@ "transform": "转换" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "统计", "label-time-field": "时间字段", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index e741a02996c..1943007d1ac 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -3532,11 +3532,11 @@ "button-create": "建立", "button-creating": "正在建立…", "cancel": "取消", - "error-invalid-folder-name": "無效的資料夾名稱", "error-required": "資料夾名稱為必填", "folder-name-input-placeholder-enter-folder-name": "輸入資料夾名稱", "label-folder-name": "資料夾名稱", "text-pull-request-created": "已建立拉取請求,其中包含對此資料夾的變更:", + "text-your-folder-will-be-created-as": "", "title-pull-request-created": "已建立拉取請求", "title-this-repository-is-read-only": "此存放庫為唯讀" }, @@ -12510,6 +12510,7 @@ "transform": "轉變" } }, + "tag": "", "time-series-table-transform-editor": { "label-stat": "統計資料", "label-time-field": "時間欄位", From 16183d794c49af7d386381e21a9eda06a9e49023 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 11 Jul 2025 11:50:00 +0100 Subject: [PATCH 12/25] E2E: fix storybook e2e playwright test (#108010) * fix storybook e2e playwright test * update action to run if test is modified --- .github/workflows/storybook-verification-playwright.yml | 2 ++ e2e-playwright/storybook/verify.spec.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/storybook-verification-playwright.yml b/.github/workflows/storybook-verification-playwright.yml index e8391259f55..e3924a67a85 100644 --- a/.github/workflows/storybook-verification-playwright.yml +++ b/.github/workflows/storybook-verification-playwright.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - 'packages/grafana-ui/**' + - 'e2e-playwright/storybook/**' - '!docs/**' - '!*.md' push: @@ -11,6 +12,7 @@ on: - main paths: - 'packages/grafana-ui/**' + - 'e2e-playwright/storybook/**' - '!docs/**' - '!*.md' diff --git a/e2e-playwright/storybook/verify.spec.ts b/e2e-playwright/storybook/verify.spec.ts index 1e24b83532d..9292cae4060 100644 --- a/e2e-playwright/storybook/verify.spec.ts +++ b/e2e-playwright/storybook/verify.spec.ts @@ -8,7 +8,7 @@ test.describe( }, () => { test('Loads the button story correctly', async ({ page }) => { - await page.goto('?path=/story/buttons-button--basic'); + await page.goto('?path=/story/inputs-button--basic'); const iframe = page.locator('#storybook-preview-iframe'); await expect(iframe).toBeVisible(); const iframeBody = iframe.contentFrame(); From 31a7f79efd4df72412f1cf7f6f6c6d7a4e036b47 Mon Sep 17 00:00:00 2001 From: Anna Urbiztondo Date: Fri, 11 Jul 2025 13:12:25 +0200 Subject: [PATCH 13/25] [Advisor]: Tile text update (#108009) Tile text update --- pkg/services/navtree/navtreeimpl/applinks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 737fd5b9782..42770b6ee96 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -341,7 +341,7 @@ func (s *ServiceImpl) readNavigationSettings() { s.navigationAppConfig["grafana-advisor-app"] = NavigationAppConfig{ SectionID: navtree.NavIDCfg, Text: "Advisor", - SubTitle: "Keep Grafana running smoothly and securely", + SubTitle: "Run checks and get suggestions to fix issues", IsNew: true, } } From c23801653211748e4142685ec78b35d306570559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 11 Jul 2025 13:34:27 +0200 Subject: [PATCH 14/25] chore(folders): add correct CODEOWNERS entry (#108018) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1c05b89a30f..cef9821d2e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -750,6 +750,7 @@ embed.go @grafana/grafana-as-code /pkg/kinds/ @grafana/grafana-as-code /pkg/registry/ @grafana/grafana-as-code /pkg/registry/apis/ @grafana/grafana-app-platform-squad +/pkg/registry/apis/folders @grafana/grafana-search-and-storage /pkg/registry/apis/query @grafana/grafana-datasources-core-services /pkg/registry/apis/secret @grafana/grafana-operator-experience-squad /pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend From c9b2126c4ad50951d0433cc3cd5f1869904a4fa2 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 11 Jul 2025 13:42:46 +0200 Subject: [PATCH 15/25] Secrets: Add v1beta1 schema with CUE + grafana-app-sdk (#107972) * Secrets: Add v1beta1 API using CUE * Secrets: Generate spec with app-sdk * Secrets: Generate decrypt proto * Secrets: Add manual files to help transition --- .github/CODEOWNERS | 1 + .github/dependabot.yml | 1 + .golangci.yml | 8 + Dockerfile | 1 + Makefile | 1 + apps/secret/Makefile | 29 + apps/secret/decrypt/v1beta1/buf.gen.yaml | 12 + apps/secret/decrypt/v1beta1/buf.yaml | 7 + apps/secret/decrypt/v1beta1/decrypt.pb.go | 311 ++++++++ apps/secret/decrypt/v1beta1/decrypt.proto | 32 + .../secret/decrypt/v1beta1/decrypt_grpc.pb.go | 110 +++ apps/secret/go.mod | 66 ++ apps/secret/go.sum | 189 +++++ apps/secret/kinds/cue.mod/module.cue | 4 + apps/secret/kinds/keeper.cue | 26 + apps/secret/kinds/manifest.cue | 10 + apps/secret/kinds/securevalue.cue | 27 + apps/secret/kinds/v1beta1/keeper.cue | 68 ++ apps/secret/kinds/v1beta1/securevalue.cue | 53 ++ .../pkg/apis/secret/v1beta1/constants.go | 18 + .../secret/v1beta1/exposed_secure_value.go | 70 ++ .../v1beta1/exposed_secure_value_test.go | 51 ++ .../apis/secret/v1beta1/keeper_codec_gen.go | 28 + .../secret/v1beta1/keeper_metadata_gen.go | 31 + .../apis/secret/v1beta1/keeper_object_gen.go | 319 ++++++++ .../apis/secret/v1beta1/keeper_schema_gen.go | 34 + .../apis/secret/v1beta1/keeper_spec_gen.go | 105 +++ .../apis/secret/v1beta1/keeper_status_gen.go | 44 ++ .../pkg/apis/secret/v1beta1/keeper_type.go | 63 ++ .../pkg/apis/secret/v1beta1/register.go | 73 ++ .../secret/v1beta1/securevalue_codec_gen.go | 28 + .../v1beta1/securevalue_metadata_gen.go | 31 + .../secret/v1beta1/securevalue_object_gen.go | 319 ++++++++ .../secret/v1beta1/securevalue_schema_gen.go | 34 + .../secret/v1beta1/securevalue_spec_gen.go | 46 ++ .../secret/v1beta1/securevalue_status_gen.go | 47 ++ .../pkg/apis/secret/v1beta1/zz_openapi_gen.go | 736 ++++++++++++++++++ apps/secret/pkg/apis/secret_manifest.go | 65 ++ go.work | 1 + go.work.sum | 35 + 40 files changed, 3134 insertions(+) create mode 100644 apps/secret/Makefile create mode 100644 apps/secret/decrypt/v1beta1/buf.gen.yaml create mode 100644 apps/secret/decrypt/v1beta1/buf.yaml create mode 100644 apps/secret/decrypt/v1beta1/decrypt.pb.go create mode 100644 apps/secret/decrypt/v1beta1/decrypt.proto create mode 100644 apps/secret/decrypt/v1beta1/decrypt_grpc.pb.go create mode 100644 apps/secret/go.mod create mode 100644 apps/secret/go.sum create mode 100644 apps/secret/kinds/cue.mod/module.cue create mode 100644 apps/secret/kinds/keeper.cue create mode 100644 apps/secret/kinds/manifest.cue create mode 100644 apps/secret/kinds/securevalue.cue create mode 100644 apps/secret/kinds/v1beta1/keeper.cue create mode 100644 apps/secret/kinds/v1beta1/securevalue.cue create mode 100644 apps/secret/pkg/apis/secret/v1beta1/constants.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_codec_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_metadata_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_object_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_schema_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_status_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/keeper_type.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/register.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/securevalue_codec_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/securevalue_metadata_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/securevalue_object_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/securevalue_schema_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go create mode 100644 apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go create mode 100644 apps/secret/pkg/apis/secret_manifest.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cef9821d2e1..f2f2e615ef9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -80,6 +80,7 @@ /apps/dashboard/ @grafana/grafana-app-platform-squad @grafana/dashboards-squad /apps/folder/ @grafana/grafana-app-platform-squad /apps/playlist/ @grafana/grafana-app-platform-squad +/apps/secret/ @grafana/grafana-operator-experience-squad /apps/investigations/ @fcjack @matryer @svennergr /apps/advisor/ @grafana/plugins-platform-backend /apps/iam/ @grafana/access-squad diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 263a75f43ab..3b1a02d0adb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,7 @@ updates: directories: - "/" - "/apps/playlist" + - "/apps/secret" - "/apps/investigations" - "/pkg/aggregator" - "/pkg/apimachinery" diff --git a/.golangci.yml b/.golangci.yml index 5bb4a5e3167..5a534ee8ec2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -83,6 +83,14 @@ linters: deny: - pkg: github.com/grafana/grafana/pkg desc: apps/playlist is not allowed to import grafana core + apps-secret: + list-mode: lax + files: + - ./apps/secret/* + - ./apps/secret/**/* + deny: + - pkg: github.com/grafana/grafana/pkg + desc: apps/secret is not allowed to import grafana core coreplugins: files: - '**/pkg/tsdb/grafana-pyroscope-datasource/*' diff --git a/Dockerfile b/Dockerfile index d98bc6db4e6..78c0ef13ad0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,6 +83,7 @@ COPY pkg/storage/unified/apistore pkg/storage/unified/apistore COPY pkg/semconv pkg/semconv COPY pkg/aggregator pkg/aggregator COPY apps/playlist apps/playlist +COPY apps/secret apps/secret COPY apps/investigations apps/investigations COPY apps/advisor apps/advisor COPY apps/dashboard apps/dashboard diff --git a/Makefile b/Makefile index 7973fd5f679..7609670ddff 100644 --- a/Makefile +++ b/Makefile @@ -472,6 +472,7 @@ protobuf: ## Compile protobuf definitions go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.4.0 buf generate pkg/plugins/backendplugin/pluginextensionv2 --template pkg/plugins/backendplugin/pluginextensionv2/buf.gen.yaml buf generate pkg/apis/secret/v0alpha1/decrypt --template pkg/apis/secret/v0alpha1/decrypt/buf.gen.yaml + buf generate apps/secret/decrypt/v1beta1 --template apps/secret/decrypt/v1beta1/buf.gen.yaml buf generate pkg/storage/unified/proto --template pkg/storage/unified/proto/buf.gen.yaml buf generate pkg/services/authz/proto/v1 --template pkg/services/authz/proto/v1/buf.gen.yaml buf generate pkg/services/ngalert/store/proto/v1 --template pkg/services/ngalert/store/proto/v1/buf.gen.yaml diff --git a/apps/secret/Makefile b/apps/secret/Makefile new file mode 100644 index 00000000000..d829f94e596 --- /dev/null +++ b/apps/secret/Makefile @@ -0,0 +1,29 @@ +APP_SDK_VERSION := v0.39.2 +APP_SDK_DIR := $(shell go env GOPATH)/bin/app-sdk-$(APP_SDK_VERSION) +APP_SDK_BIN := $(APP_SDK_DIR)/grafana-app-sdk + +.PHONY: install-app-sdk +install-app-sdk: $(APP_SDK_BIN) ## Install the Grafana App SDK + +$(APP_SDK_BIN): + @echo "Installing Grafana App SDK version $(APP_SDK_VERSION)" + @mkdir -p $(APP_SDK_DIR) + # The only way to install specific versions of binaries using `go install` + # is by setting GOBIN to the directory you want to install the binary to. + GOBIN=$(APP_SDK_DIR) go install github.com/grafana/grafana-app-sdk/cmd/grafana-app-sdk@$(APP_SDK_VERSION) + @touch $@ + +.PHONY: update-app-sdk +update-app-sdk: ## Update the Grafana App SDK dependency in go.mod + go get github.com/grafana/grafana-app-sdk@$(APP_SDK_VERSION) + go mod tidy + +.PHONY: generate +generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generation + @$(APP_SDK_BIN) generate \ + --source=./kinds/ \ + --gogenpath=./pkg/apis \ + --grouping=group \ + --defencoding=none \ + --noschemasinmanifest \ + --postprocess \ No newline at end of file diff --git a/apps/secret/decrypt/v1beta1/buf.gen.yaml b/apps/secret/decrypt/v1beta1/buf.gen.yaml new file mode 100644 index 00000000000..b2691bae669 --- /dev/null +++ b/apps/secret/decrypt/v1beta1/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v1 + +plugins: + - plugin: go + out: apps/secret/decrypt/v1beta1 + opt: + - paths=source_relative + - plugin: go-grpc + out: apps/secret/decrypt/v1beta1 + opt: + - paths=source_relative + - require_unimplemented_servers=false diff --git a/apps/secret/decrypt/v1beta1/buf.yaml b/apps/secret/decrypt/v1beta1/buf.yaml new file mode 100644 index 00000000000..1a5194568a9 --- /dev/null +++ b/apps/secret/decrypt/v1beta1/buf.yaml @@ -0,0 +1,7 @@ +version: v1 +breaking: + use: + - FILE +lint: + use: + - DEFAULT diff --git a/apps/secret/decrypt/v1beta1/decrypt.pb.go b/apps/secret/decrypt/v1beta1/decrypt.pb.go new file mode 100644 index 00000000000..4e334ff7ca3 --- /dev/null +++ b/apps/secret/decrypt/v1beta1/decrypt.pb.go @@ -0,0 +1,311 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc (unknown) +// source: decrypt.proto + +package decryptv1beta1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SecureValueDecryptRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The stack_id or org_id. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // A list of secure value names to decrypt. + Names []string `protobuf:"bytes,2,rep,name=names,proto3" json:"names,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecureValueDecryptRequest) Reset() { + *x = SecureValueDecryptRequest{} + mi := &file_decrypt_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecureValueDecryptRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecureValueDecryptRequest) ProtoMessage() {} + +func (x *SecureValueDecryptRequest) ProtoReflect() protoreflect.Message { + mi := &file_decrypt_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecureValueDecryptRequest.ProtoReflect.Descriptor instead. +func (*SecureValueDecryptRequest) Descriptor() ([]byte, []int) { + return file_decrypt_proto_rawDescGZIP(), []int{0} +} + +func (x *SecureValueDecryptRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *SecureValueDecryptRequest) GetNames() []string { + if x != nil { + return x.Names + } + return nil +} + +type SecureValueDecryptResponseCollection struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A map of secure value names and their decrypted values. + // The value will be an error message if the requestor does not have permissions to read it, or if the value does not exist. + // It will never return a 404 Not Found to avoid scanning of valid secure values. + DecryptedValues map[string]*Result `protobuf:"bytes,1,rep,name=decrypted_values,json=decryptedValues,proto3" json:"decrypted_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecureValueDecryptResponseCollection) Reset() { + *x = SecureValueDecryptResponseCollection{} + mi := &file_decrypt_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecureValueDecryptResponseCollection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecureValueDecryptResponseCollection) ProtoMessage() {} + +func (x *SecureValueDecryptResponseCollection) ProtoReflect() protoreflect.Message { + mi := &file_decrypt_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecureValueDecryptResponseCollection.ProtoReflect.Descriptor instead. +func (*SecureValueDecryptResponseCollection) Descriptor() ([]byte, []int) { + return file_decrypt_proto_rawDescGZIP(), []int{1} +} + +func (x *SecureValueDecryptResponseCollection) GetDecryptedValues() map[string]*Result { + if x != nil { + return x.DecryptedValues + } + return nil +} + +type Result struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *Result_Value + // *Result_ErrorMessage + Result isResult_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Result) Reset() { + *x = Result{} + mi := &file_decrypt_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Result) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Result) ProtoMessage() {} + +func (x *Result) ProtoReflect() protoreflect.Message { + mi := &file_decrypt_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Result.ProtoReflect.Descriptor instead. +func (*Result) Descriptor() ([]byte, []int) { + return file_decrypt_proto_rawDescGZIP(), []int{2} +} + +func (x *Result) GetResult() isResult_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *Result) GetValue() string { + if x != nil { + if x, ok := x.Result.(*Result_Value); ok { + return x.Value + } + } + return "" +} + +func (x *Result) GetErrorMessage() string { + if x != nil { + if x, ok := x.Result.(*Result_ErrorMessage); ok { + return x.ErrorMessage + } + } + return "" +} + +type isResult_Result interface { + isResult_Result() +} + +type Result_Value struct { + Value string `protobuf:"bytes,1,opt,name=value,proto3,oneof"` +} + +type Result_ErrorMessage struct { + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3,oneof"` +} + +func (*Result_Value) isResult_Result() {} + +func (*Result_ErrorMessage) isResult_Result() {} + +var File_decrypt_proto protoreflect.FileDescriptor + +var file_decrypt_proto_rawDesc = string([]byte{ + 0x0a, 0x0d, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x22, + 0x4f, 0x0a, 0x19, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, + 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x22, 0xf8, 0x01, 0x0a, 0x24, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x74, 0x0a, 0x10, 0x64, 0x65, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, + 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, + 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, + 0x5a, 0x0a, 0x14, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x51, 0x0a, 0x06, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x16, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x25, 0x0a, + 0x0d, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0x8e, + 0x01, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x72, 0x12, 0x76, 0x0a, 0x13, 0x44, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x29, + 0x2e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, + 0x53, 0x65, 0x63, 0x75, 0x72, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x64, 0x65, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x44, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, + 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x61, 0x70, + 0x70, 0x73, 0x2f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2f, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x3b, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_decrypt_proto_rawDescOnce sync.Once + file_decrypt_proto_rawDescData []byte +) + +func file_decrypt_proto_rawDescGZIP() []byte { + file_decrypt_proto_rawDescOnce.Do(func() { + file_decrypt_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_decrypt_proto_rawDesc), len(file_decrypt_proto_rawDesc))) + }) + return file_decrypt_proto_rawDescData +} + +var file_decrypt_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_decrypt_proto_goTypes = []any{ + (*SecureValueDecryptRequest)(nil), // 0: decryptv1beta1.SecureValueDecryptRequest + (*SecureValueDecryptResponseCollection)(nil), // 1: decryptv1beta1.SecureValueDecryptResponseCollection + (*Result)(nil), // 2: decryptv1beta1.Result + nil, // 3: decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry +} +var file_decrypt_proto_depIdxs = []int32{ + 3, // 0: decryptv1beta1.SecureValueDecryptResponseCollection.decrypted_values:type_name -> decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry + 2, // 1: decryptv1beta1.SecureValueDecryptResponseCollection.DecryptedValuesEntry.value:type_name -> decryptv1beta1.Result + 0, // 2: decryptv1beta1.SecureValueDecrypter.DecryptSecureValues:input_type -> decryptv1beta1.SecureValueDecryptRequest + 1, // 3: decryptv1beta1.SecureValueDecrypter.DecryptSecureValues:output_type -> decryptv1beta1.SecureValueDecryptResponseCollection + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_decrypt_proto_init() } +func file_decrypt_proto_init() { + if File_decrypt_proto != nil { + return + } + file_decrypt_proto_msgTypes[2].OneofWrappers = []any{ + (*Result_Value)(nil), + (*Result_ErrorMessage)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_decrypt_proto_rawDesc), len(file_decrypt_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_decrypt_proto_goTypes, + DependencyIndexes: file_decrypt_proto_depIdxs, + MessageInfos: file_decrypt_proto_msgTypes, + }.Build() + File_decrypt_proto = out.File + file_decrypt_proto_goTypes = nil + file_decrypt_proto_depIdxs = nil +} diff --git a/apps/secret/decrypt/v1beta1/decrypt.proto b/apps/secret/decrypt/v1beta1/decrypt.proto new file mode 100644 index 00000000000..a8ee64de420 --- /dev/null +++ b/apps/secret/decrypt/v1beta1/decrypt.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package decryptv1beta1; + +option go_package = "github.com/grafana/grafana/apps/secret/decrypt/v1beta1;decryptv1beta1"; + +message SecureValueDecryptRequest { + // The stack_id or org_id. + string namespace = 1; + + // A list of secure value names to decrypt. + repeated string names = 2; +} + +message SecureValueDecryptResponseCollection { + // A map of secure value names and their decrypted values. + // The value will be an error message if the requestor does not have permissions to read it, or if the value does not exist. + // It will never return a 404 Not Found to avoid scanning of valid secure values. + map decrypted_values = 1; +} + +message Result { + oneof result { + string value = 1; + string error_message = 2; + } +} + +service SecureValueDecrypter { + // Decrypts a list of secure values and returns them as a map. + rpc DecryptSecureValues(SecureValueDecryptRequest) returns (SecureValueDecryptResponseCollection); +} diff --git a/apps/secret/decrypt/v1beta1/decrypt_grpc.pb.go b/apps/secret/decrypt/v1beta1/decrypt_grpc.pb.go new file mode 100644 index 00000000000..92092215b58 --- /dev/null +++ b/apps/secret/decrypt/v1beta1/decrypt_grpc.pb.go @@ -0,0 +1,110 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.4.0 +// - protoc (unknown) +// source: decrypt.proto + +package decryptv1beta1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.62.0 or later. +const _ = grpc.SupportPackageIsVersion8 + +const ( + SecureValueDecrypter_DecryptSecureValues_FullMethodName = "/decryptv1beta1.SecureValueDecrypter/DecryptSecureValues" +) + +// SecureValueDecrypterClient is the client API for SecureValueDecrypter service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SecureValueDecrypterClient interface { + // Decrypts a list of secure values and returns them as a map. + DecryptSecureValues(ctx context.Context, in *SecureValueDecryptRequest, opts ...grpc.CallOption) (*SecureValueDecryptResponseCollection, error) +} + +type secureValueDecrypterClient struct { + cc grpc.ClientConnInterface +} + +func NewSecureValueDecrypterClient(cc grpc.ClientConnInterface) SecureValueDecrypterClient { + return &secureValueDecrypterClient{cc} +} + +func (c *secureValueDecrypterClient) DecryptSecureValues(ctx context.Context, in *SecureValueDecryptRequest, opts ...grpc.CallOption) (*SecureValueDecryptResponseCollection, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SecureValueDecryptResponseCollection) + err := c.cc.Invoke(ctx, SecureValueDecrypter_DecryptSecureValues_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SecureValueDecrypterServer is the server API for SecureValueDecrypter service. +// All implementations should embed UnimplementedSecureValueDecrypterServer +// for forward compatibility +type SecureValueDecrypterServer interface { + // Decrypts a list of secure values and returns them as a map. + DecryptSecureValues(context.Context, *SecureValueDecryptRequest) (*SecureValueDecryptResponseCollection, error) +} + +// UnimplementedSecureValueDecrypterServer should be embedded to have forward compatible implementations. +type UnimplementedSecureValueDecrypterServer struct { +} + +func (UnimplementedSecureValueDecrypterServer) DecryptSecureValues(context.Context, *SecureValueDecryptRequest) (*SecureValueDecryptResponseCollection, error) { + return nil, status.Errorf(codes.Unimplemented, "method DecryptSecureValues not implemented") +} + +// UnsafeSecureValueDecrypterServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SecureValueDecrypterServer will +// result in compilation errors. +type UnsafeSecureValueDecrypterServer interface { + mustEmbedUnimplementedSecureValueDecrypterServer() +} + +func RegisterSecureValueDecrypterServer(s grpc.ServiceRegistrar, srv SecureValueDecrypterServer) { + s.RegisterService(&SecureValueDecrypter_ServiceDesc, srv) +} + +func _SecureValueDecrypter_DecryptSecureValues_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SecureValueDecryptRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SecureValueDecrypterServer).DecryptSecureValues(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SecureValueDecrypter_DecryptSecureValues_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SecureValueDecrypterServer).DecryptSecureValues(ctx, req.(*SecureValueDecryptRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SecureValueDecrypter_ServiceDesc is the grpc.ServiceDesc for SecureValueDecrypter service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SecureValueDecrypter_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "decryptv1beta1.SecureValueDecrypter", + HandlerType: (*SecureValueDecrypterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "DecryptSecureValues", + Handler: _SecureValueDecrypter_DecryptSecureValues_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "decrypt.proto", +} diff --git a/apps/secret/go.mod b/apps/secret/go.mod new file mode 100644 index 00000000000..27e23bb6910 --- /dev/null +++ b/apps/secret/go.mod @@ -0,0 +1,66 @@ +module github.com/grafana/grafana/apps/secret + +go 1.24.4 + +require ( + github.com/grafana/grafana-app-sdk v0.39.2 + github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf + github.com/stretchr/testify v1.10.0 + google.golang.org/grpc v1.73.0 + google.golang.org/protobuf v1.36.6 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apimachinery v0.33.2 + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/getkin/kin-openapi v0.132.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-test/deep v1.1.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/grafana/grafana-app-sdk/logging v0.39.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/client-go v0.33.2 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/apps/secret/go.sum b/apps/secret/go.sum new file mode 100644 index 00000000000..c2439606f70 --- /dev/null +++ b/apps/secret/go.sum @@ -0,0 +1,189 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= +github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-app-sdk v0.39.2 h1:ymfr+1318t+JC9U2OYrzVpGmNG/aJONUmFFu/G98Xh8= +github.com/grafana/grafana-app-sdk v0.39.2/go.mod h1:t0m6q561lpoHQCixS9LUHFUhUzDClzNtm7BH60gHVSY= +github.com/grafana/grafana-app-sdk/logging v0.39.1 h1:lI5rbrheuwVPuyIM6LIuEYOCSpgmXahfKtqeMyhbGPU= +github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf h1:BBGDHffvVNLoYQlXEpbXcxE0vbpq7pm/8OWF5I+UDZg= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/apps/secret/kinds/cue.mod/module.cue b/apps/secret/kinds/cue.mod/module.cue new file mode 100644 index 00000000000..bad5ef304e1 --- /dev/null +++ b/apps/secret/kinds/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "github.com/grafana/grafana/apps/secret/kinds" +language: { + version: "v0.9.0" +} diff --git a/apps/secret/kinds/keeper.cue b/apps/secret/kinds/keeper.cue new file mode 100644 index 00000000000..117e3bbd8a3 --- /dev/null +++ b/apps/secret/kinds/keeper.cue @@ -0,0 +1,26 @@ +package secret + +import "github.com/grafana/grafana/apps/secret/kinds/v1beta1" + +keeper: { + kind: "Keeper" + pluralName: "Keepers" + current: "v1beta1" + scope: "Namespaced" + current: "v1beta1" + codegen: { + ts: { + enabled: false + } + go: { + enabled: true + } + } + versions: { + "v1beta1": { + schema: { + spec: v1beta1.KeeperSpec + } + } + } +} diff --git a/apps/secret/kinds/manifest.cue b/apps/secret/kinds/manifest.cue new file mode 100644 index 00000000000..1f288bcd328 --- /dev/null +++ b/apps/secret/kinds/manifest.cue @@ -0,0 +1,10 @@ +package secret + +manifest: { + appName: "secret" + groupOverride: "secret.grafana.app" + kinds: [ + securevalue, + keeper, + ] +} diff --git a/apps/secret/kinds/securevalue.cue b/apps/secret/kinds/securevalue.cue new file mode 100644 index 00000000000..6bed093f46d --- /dev/null +++ b/apps/secret/kinds/securevalue.cue @@ -0,0 +1,27 @@ +package secret + +import "github.com/grafana/grafana/apps/secret/kinds/v1beta1" + +securevalue: { + kind: "SecureValue" + pluralName: "SecureValues" + current: "v1beta1" + scope: "Namespaced" + current: "v1beta1" + codegen: { + ts: { + enabled: false + } + go: { + enabled: true + } + } + versions: { + "v1beta1": { + schema: { + spec: v1beta1.SecureValueSpec + status: v1beta1.SecureValueStatus + } + } + } +} diff --git a/apps/secret/kinds/v1beta1/keeper.cue b/apps/secret/kinds/v1beta1/keeper.cue new file mode 100644 index 00000000000..36e20198ab7 --- /dev/null +++ b/apps/secret/kinds/v1beta1/keeper.cue @@ -0,0 +1,68 @@ +package v1beta1 + +import "strings" + +KeeperSpec: { + // Short description for the Keeper. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + description: string & strings.MinRunes(1) & strings.MaxRunes(253) + + // AWS Keeper Configuration. + // +structType=atomic + // +optional + aws?: #AWSConfig + + // Azure Keeper Configuration. + // +structType=atomic + // +optional + azure?: #AzureConfig + + // GCP Keeper Configuration. + // +structType=atomic + // +optional + gcp?: #GCPConfig + + // HashiCorp Vault Keeper Configuration. + // +structType=atomic + // +optional + hashiCorpVault?: #HashiCorpConfig +} + +#AWSConfig: { + accessKeyID: #CredentialValue + secretAccessKey: #CredentialValue + kmsKeyID?: string +} + +#AzureConfig: { + keyVaultName: string + tenantID: string + clientID: string + clientSecret: #CredentialValue +} + +#GCPConfig: { + projectID: string + credentialsFile: string +} + +#HashiCorpConfig: { + address: string + token: #CredentialValue +} + +#CredentialValue: { + // The name of the secure value that holds the actual value. + // +optional + secureValueName: string + + // The value is taken from the environment variable. + // +optional + valueFromEnv: string + + // The value is taken from the Grafana config file. + // TODO: how do we explain that this is a path to the config file? + // +optional + valueFromConfig: string +} diff --git a/apps/secret/kinds/v1beta1/securevalue.cue b/apps/secret/kinds/v1beta1/securevalue.cue new file mode 100644 index 00000000000..9fef8e63f46 --- /dev/null +++ b/apps/secret/kinds/v1beta1/securevalue.cue @@ -0,0 +1,53 @@ +package v1beta1 + +import ( + "list" + "strings" +) + +// ExposedSecureValue contains the raw decrypted secure value. +#ExposedSecureValue: string + +SecureValueSpec: { + // Short description that explains the purpose of this SecureValue. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=25 + description: string & strings.MinRunes(1) & strings.MaxRunes(25) + + // The raw value is only valid for write. Read/List will always be empty. + // There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. + // Minimum and maximum lengths in bytes. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=24576 + // +optional + value?: #ExposedSecureValue + + // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. + // This should not contain sensitive information. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=1024 + // +optional + ref?: string & strings.MinRunes(1) & strings.MaxRunes(1024) + + // Name of the keeper, being the actual storage of the secure value. + // If not specified, the default keeper for the namespace will be used. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + // +optional + keeper?: string & strings.MinRunes(1) & strings.MaxRunes(253) + + // The Decrypters that are allowed to decrypt this secret. + // An empty list means no service can decrypt it. + // +k8s:validation:maxItems=64 + // +k8s:validation:uniqueItems=true + // +listType=atomic + // +optional + decrypters?: [...string] & list.UniqueItems() & list.MaxItems(64) +} + +SecureValueStatus: { + version: int64 & >=0 + + // +optional + externalID: string +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/constants.go b/apps/secret/pkg/apis/secret/v1beta1/constants.go new file mode 100644 index 00000000000..2db86acf35b --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/constants.go @@ -0,0 +1,18 @@ +package v1beta1 + +import "k8s.io/apimachinery/pkg/runtime/schema" + +const ( + // APIGroup is the API group used by all kinds in this package + APIGroup = "secret.grafana.app" + // APIVersion is the API version used by all kinds in this package + APIVersion = "v1beta1" +) + +var ( + // GroupVersion is a schema.GroupVersion consisting of the Group and Version constants for this package + GroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } +) diff --git a/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go new file mode 100644 index 00000000000..a8738c3093a --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value.go @@ -0,0 +1,70 @@ +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 + +import ( + "encoding/json" + "fmt" + "strconv" + + "gopkg.in/yaml.v3" +) + +const redacted = "[REDACTED]" + +// ExposedSecureValue contains the raw decrypted secure value. +type ExposedSecureValue = SecureValueExposedSecureValue + +var ( + _ fmt.Stringer = (*ExposedSecureValue)(nil) + _ fmt.Formatter = (*ExposedSecureValue)(nil) + _ fmt.GoStringer = (*ExposedSecureValue)(nil) + _ json.Marshaler = (*ExposedSecureValue)(nil) + _ yaml.Marshaler = (*ExposedSecureValue)(nil) +) + +// NewExposedSecureValue creates a new exposed secure value wrapper. +func NewExposedSecureValue(v string) ExposedSecureValue { + return ExposedSecureValue(v) +} + +// DangerouslyExposeAndConsumeValue will move the decrypted secure value out of the wrapper and return it. +// Further attempts to call this method will panic. +// The function name is intentionally kept long and weird because this is a dangerous operation and should be used carefully! +func (s *ExposedSecureValue) DangerouslyExposeAndConsumeValue() string { + if *s == "" { + panic("underlying value is empty or was consumed") + } + + tmp := *s + *s = "" + + return string(tmp) +} + +// String must not return the exposed secure value. +func (s ExposedSecureValue) String() string { + return redacted +} + +// Format must not return the exposed secure value. +func (s ExposedSecureValue) Format(f fmt.State, _verb rune) { + _, _ = fmt.Fprint(f, redacted) +} + +// GoString must not return the exposed secure value. +func (s ExposedSecureValue) GoString() string { + return redacted +} + +// MarshalJSON must not return the exposed secure value. +func (s ExposedSecureValue) MarshalJSON() ([]byte, error) { + return []byte(strconv.Quote(redacted)), nil +} + +// MarshalYAML must not return the exposed secure value. +func (s ExposedSecureValue) MarshalYAML() (any, error) { + return redacted, nil +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go new file mode 100644 index 00000000000..90d53bdf7ae --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/exposed_secure_value_test.go @@ -0,0 +1,51 @@ +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestExposedSecureValue(t *testing.T) { + expected := "[REDACTED]" + + rawValue := "a-password" + esv := NewExposedSecureValue(rawValue) + + // String must not return the exposed secure value. + require.Equal(t, expected, esv.String()) + + // Format/GoString must not return the exposed secure value. + require.Equal(t, expected, fmt.Sprintf("%+#v", esv)) + require.Equal(t, expected, fmt.Sprintf("%v", esv)) + require.Equal(t, expected, fmt.Sprintf("%s", esv)) + + buf := new(bytes.Buffer) + _, err := fmt.Fprintf(buf, "%#v", esv) + require.NoError(t, err) + require.Equal(t, expected, buf.String()) + + // MarshalJSON must not return the exposed secure value. + bytes, err := json.Marshal(esv) + require.NoError(t, err) + require.Equal(t, `"`+expected+`"`, string(bytes)) + + // MarshalYAML must not return the exposed secure value. + bytes, err = yaml.Marshal(esv) + require.NoError(t, err) + require.Equal(t, "'"+expected+"'\n", string(bytes)) + + // DangerouslyExposeAndConsumeValue returns the raw value. + require.Equal(t, rawValue, esv.DangerouslyExposeAndConsumeValue()) + + // Further calls to DangerouslyExposeAndConsumeValue will panic. + require.Panics(t, func() { esv.DangerouslyExposeAndConsumeValue() }) +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_codec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_codec_gen.go new file mode 100644 index 00000000000..1a4f6fb2704 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// KeeperJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type KeeperJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*KeeperJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*KeeperJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &KeeperJSONCodec{} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_metadata_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_metadata_gen.go new file mode 100644 index 00000000000..82bd4258403 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type KeeperMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewKeeperMetadata creates a new KeeperMetadata object. +func NewKeeperMetadata() *KeeperMetadata { + return &KeeperMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_object_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_object_gen.go new file mode 100644 index 00000000000..f75d6299345 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Keeper struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Keeper + Spec KeeperSpec `json:"spec" yaml:"spec"` + + Status KeeperStatus `json:"status" yaml:"status"` +} + +func (o *Keeper) GetSpec() any { + return o.Spec +} + +func (o *Keeper) SetSpec(spec any) error { + cast, ok := spec.(KeeperSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Keeper) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *Keeper) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *Keeper) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(KeeperStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type KeeperStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Keeper) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Keeper) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Keeper) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Keeper) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Keeper) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Keeper) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Keeper) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Keeper) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Keeper) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Keeper) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Keeper) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Keeper) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Keeper) DeepCopy() *Keeper { + cpy := &Keeper{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Keeper) DeepCopyInto(dst *Keeper) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &Keeper{} + +// +k8s:openapi-gen=true +type KeeperList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Keeper `json:"items" yaml:"items"` +} + +func (o *KeeperList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *KeeperList) Copy() resource.ListObject { + cpy := &KeeperList{ + TypeMeta: o.TypeMeta, + Items: make([]Keeper, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Keeper); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *KeeperList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *KeeperList) SetItems(items []resource.Object) { + o.Items = make([]Keeper, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Keeper) + } +} + +func (o *KeeperList) DeepCopy() *KeeperList { + cpy := &KeeperList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *KeeperList) DeepCopyInto(dst *KeeperList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &KeeperList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *KeeperSpec) DeepCopy() *KeeperSpec { + cpy := &KeeperSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *KeeperSpec) DeepCopyInto(dst *KeeperSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of KeeperStatus +func (s *KeeperStatus) DeepCopy() *KeeperStatus { + cpy := &KeeperStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies KeeperStatus into another KeeperStatus object +func (s *KeeperStatus) DeepCopyInto(dst *KeeperStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_schema_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_schema_gen.go new file mode 100644 index 00000000000..b2ef185503e --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaKeeper = resource.NewSimpleSchema("secret.grafana.app", "v1beta1", &Keeper{}, &KeeperList{}, resource.WithKind("Keeper"), + resource.WithPlural("keepers"), resource.WithScope(resource.NamespacedScope)) + kindKeeper = resource.Kind{ + Schema: schemaKeeper, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &KeeperJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func KeeperKind() resource.Kind { + return kindKeeper +} + +// Schema returns a resource.SimpleSchema representation of Keeper +func KeeperSchema() *resource.SimpleSchema { + return schemaKeeper +} + +// Interface compliance checks +var _ resource.Schema = kindKeeper diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go new file mode 100644 index 00000000000..76d232465c8 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_spec_gen.go @@ -0,0 +1,105 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// +k8s:openapi-gen=true +type KeeperAWSConfig struct { + AccessKeyID KeeperCredentialValue `json:"accessKeyID"` + SecretAccessKey KeeperCredentialValue `json:"secretAccessKey"` + KmsKeyID *string `json:"kmsKeyID,omitempty"` +} + +// NewKeeperAWSConfig creates a new KeeperAWSConfig object. +func NewKeeperAWSConfig() *KeeperAWSConfig { + return &KeeperAWSConfig{ + AccessKeyID: *NewKeeperCredentialValue(), + SecretAccessKey: *NewKeeperCredentialValue(), + } +} + +// +k8s:openapi-gen=true +type KeeperCredentialValue struct { + // The name of the secure value that holds the actual value. + // +optional + SecureValueName string `json:"secureValueName"` + // The value is taken from the environment variable. + // +optional + ValueFromEnv string `json:"valueFromEnv"` + // The value is taken from the Grafana config file. + // TODO: how do we explain that this is a path to the config file? + // +optional + ValueFromConfig string `json:"valueFromConfig"` +} + +// NewKeeperCredentialValue creates a new KeeperCredentialValue object. +func NewKeeperCredentialValue() *KeeperCredentialValue { + return &KeeperCredentialValue{} +} + +// +k8s:openapi-gen=true +type KeeperAzureConfig struct { + KeyVaultName string `json:"keyVaultName"` + TenantID string `json:"tenantID"` + ClientID string `json:"clientID"` + ClientSecret KeeperCredentialValue `json:"clientSecret"` +} + +// NewKeeperAzureConfig creates a new KeeperAzureConfig object. +func NewKeeperAzureConfig() *KeeperAzureConfig { + return &KeeperAzureConfig{ + ClientSecret: *NewKeeperCredentialValue(), + } +} + +// +k8s:openapi-gen=true +type KeeperGCPConfig struct { + ProjectID string `json:"projectID"` + CredentialsFile string `json:"credentialsFile"` +} + +// NewKeeperGCPConfig creates a new KeeperGCPConfig object. +func NewKeeperGCPConfig() *KeeperGCPConfig { + return &KeeperGCPConfig{} +} + +// +k8s:openapi-gen=true +type KeeperHashiCorpConfig struct { + Address string `json:"address"` + Token KeeperCredentialValue `json:"token"` +} + +// NewKeeperHashiCorpConfig creates a new KeeperHashiCorpConfig object. +func NewKeeperHashiCorpConfig() *KeeperHashiCorpConfig { + return &KeeperHashiCorpConfig{ + Token: *NewKeeperCredentialValue(), + } +} + +// +k8s:openapi-gen=true +type KeeperSpec struct { + // Short description for the Keeper. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + Description string `json:"description"` + // AWS Keeper Configuration. + // +structType=atomic + // +optional + Aws *KeeperAWSConfig `json:"aws,omitempty"` + // Azure Keeper Configuration. + // +structType=atomic + // +optional + Azure *KeeperAzureConfig `json:"azure,omitempty"` + // GCP Keeper Configuration. + // +structType=atomic + // +optional + Gcp *KeeperGCPConfig `json:"gcp,omitempty"` + // HashiCorp Vault Keeper Configuration. + // +structType=atomic + // +optional + HashiCorpVault *KeeperHashiCorpConfig `json:"hashiCorpVault,omitempty"` +} + +// NewKeeperSpec creates a new KeeperSpec object. +func NewKeeperSpec() *KeeperSpec { + return &KeeperSpec{} +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_status_gen.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_status_gen.go new file mode 100644 index 00000000000..033b71b183b --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_status_gen.go @@ -0,0 +1,44 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// +k8s:openapi-gen=true +type KeeperstatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State KeeperStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewKeeperstatusOperatorState creates a new KeeperstatusOperatorState object. +func NewKeeperstatusOperatorState() *KeeperstatusOperatorState { + return &KeeperstatusOperatorState{} +} + +// +k8s:openapi-gen=true +type KeeperStatus struct { + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]KeeperstatusOperatorState `json:"operatorStates,omitempty"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewKeeperStatus creates a new KeeperStatus object. +func NewKeeperStatus() *KeeperStatus { + return &KeeperStatus{} +} + +// +k8s:openapi-gen=true +type KeeperStatusOperatorStateState string + +const ( + KeeperStatusOperatorStateStateSuccess KeeperStatusOperatorStateState = "success" + KeeperStatusOperatorStateStateInProgress KeeperStatusOperatorStateState = "in_progress" + KeeperStatusOperatorStateStateFailed KeeperStatusOperatorStateState = "failed" +) diff --git a/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go b/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go new file mode 100644 index 00000000000..0feeac75dd1 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/keeper_type.go @@ -0,0 +1,63 @@ +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 + +// KeeperType represents the type of a Keeper. +type KeeperType string + +const ( + AWSKeeperType KeeperType = "aws" + AzureKeeperType KeeperType = "azure" + GCPKeeperType KeeperType = "gcp" + HashiCorpKeeperType KeeperType = "hashicorp" +) + +func (kt KeeperType) String() string { + return string(kt) +} + +// KeeperConfig is an interface that all keeper config types must implement. +type KeeperConfig interface { + Type() KeeperType +} + +func (s *KeeperSpec) GetType() KeeperType { + if s.Aws != nil { + return AWSKeeperType + } + if s.Azure != nil { + return AzureKeeperType + } + if s.Gcp != nil { + return GCPKeeperType + } + if s.HashiCorpVault != nil { + return HashiCorpKeeperType + } + return "" +} + +// System Keeper. +type SystemKeeperConfig struct{} + +func (*SystemKeeperConfig) Type() KeeperType { + return "system" +} + +func (s *KeeperAWSConfig) Type() KeeperType { + return AWSKeeperType +} + +func (s *KeeperAzureConfig) Type() KeeperType { + return AzureKeeperType +} + +func (s *KeeperGCPConfig) Type() KeeperType { + return GCPKeeperType +} + +func (s *KeeperHashiCorpConfig) Type() KeeperType { + return HashiCorpKeeperType +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/register.go b/apps/secret/pkg/apis/secret/v1beta1/register.go new file mode 100644 index 00000000000..32d89abf7d5 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/register.go @@ -0,0 +1,73 @@ +// +// THIS FILE IS MANUALLY GENERATED TO OVERCOME LIMITATIONS WITH CUE. FEEL FREE TO EDIT IT. +// + +package v1beta1 + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana/pkg/apimachinery/utils" +) + +var SecureValuesResourceInfo = utils.NewResourceInfo( + APIGroup, APIVersion, + "securevalues", "securevalue", "SecureValue", + func() runtime.Object { return &SecureValue{} }, + func() runtime.Object { return &SecureValueList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"}, + {Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"}, + {Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"}, + }, + Reader: func(obj any) ([]any, error) { + if r, ok := obj.(*SecureValue); ok { + return []any{r.Name, r.Spec.Description, r.Spec.Keeper, r.Spec.Ref}, nil + } + + return nil, fmt.Errorf("expected SecureValue but got %T", obj) + }, + }, +) + +var KeeperResourceInfo = utils.NewResourceInfo( + APIGroup, APIVersion, + "keepers", "keeper", "Keeper", + func() runtime.Object { return &Keeper{} }, + func() runtime.Object { return &KeeperList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Description", Type: "string", Format: "string", Description: "Short description for the Keeper"}, + }, + Reader: func(obj any) ([]any, error) { + if r, ok := obj.(*Keeper); ok { + return []any{r.Name, r.Spec.Description}, nil + } + + return nil, fmt.Errorf("expected Keeper but got %T", obj) + }, + }, +) + +// SchemeGroupVersion is group version used to register these objects. +var SchemeGroupVersion = schema.GroupVersion{Group: APIGroup, Version: APIVersion} + +// Adds the list of known types to the given scheme. +func AddKnownTypes(scheme *runtime.Scheme, version string) error { + scheme.AddKnownTypes( + schema.GroupVersion{Group: APIGroup, Version: version}, + &SecureValue{}, + &SecureValueList{}, + &Keeper{}, + &KeeperList{}, + ) + + return nil +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_codec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_codec_gen.go new file mode 100644 index 00000000000..8e9c88ed873 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// SecureValueJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type SecureValueJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*SecureValueJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*SecureValueJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &SecureValueJSONCodec{} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_metadata_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_metadata_gen.go new file mode 100644 index 00000000000..c8691021594 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type SecureValueMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewSecureValueMetadata creates a new SecureValueMetadata object. +func NewSecureValueMetadata() *SecureValueMetadata { + return &SecureValueMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_object_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_object_gen.go new file mode 100644 index 00000000000..c1832e9bca7 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_object_gen.go @@ -0,0 +1,319 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type SecureValue struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the SecureValue + Spec SecureValueSpec `json:"spec" yaml:"spec"` + + Status SecureValueStatus `json:"status" yaml:"status"` +} + +func (o *SecureValue) GetSpec() any { + return o.Spec +} + +func (o *SecureValue) SetSpec(spec any) error { + cast, ok := spec.(SecureValueSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *SecureValue) GetSubresources() map[string]any { + return map[string]any{ + "status": o.Status, + } +} + +func (o *SecureValue) GetSubresource(name string) (any, bool) { + switch name { + case "status": + return o.Status, true + default: + return nil, false + } +} + +func (o *SecureValue) SetSubresource(name string, value any) error { + switch name { + case "status": + cast, ok := value.(SecureValueStatus) + if !ok { + return fmt.Errorf("cannot set status type %#v, not of type SecureValueStatus", value) + } + o.Status = cast + return nil + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *SecureValue) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *SecureValue) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *SecureValue) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *SecureValue) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *SecureValue) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *SecureValue) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *SecureValue) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *SecureValue) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *SecureValue) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *SecureValue) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *SecureValue) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *SecureValue) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *SecureValue) DeepCopy() *SecureValue { + cpy := &SecureValue{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *SecureValue) DeepCopyInto(dst *SecureValue) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) + o.Status.DeepCopyInto(&dst.Status) +} + +// Interface compliance compile-time check +var _ resource.Object = &SecureValue{} + +// +k8s:openapi-gen=true +type SecureValueList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []SecureValue `json:"items" yaml:"items"` +} + +func (o *SecureValueList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *SecureValueList) Copy() resource.ListObject { + cpy := &SecureValueList{ + TypeMeta: o.TypeMeta, + Items: make([]SecureValue, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*SecureValue); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *SecureValueList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *SecureValueList) SetItems(items []resource.Object) { + o.Items = make([]SecureValue, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*SecureValue) + } +} + +func (o *SecureValueList) DeepCopy() *SecureValueList { + cpy := &SecureValueList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *SecureValueList) DeepCopyInto(dst *SecureValueList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &SecureValueList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *SecureValueSpec) DeepCopy() *SecureValueSpec { + cpy := &SecureValueSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *SecureValueSpec) DeepCopyInto(dst *SecureValueSpec) { + resource.CopyObjectInto(dst, s) +} + +// DeepCopy creates a full deep copy of SecureValueStatus +func (s *SecureValueStatus) DeepCopy() *SecureValueStatus { + cpy := &SecureValueStatus{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies SecureValueStatus into another SecureValueStatus object +func (s *SecureValueStatus) DeepCopyInto(dst *SecureValueStatus) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_schema_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_schema_gen.go new file mode 100644 index 00000000000..4a978fe34b6 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v1beta1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaSecureValue = resource.NewSimpleSchema("secret.grafana.app", "v1beta1", &SecureValue{}, &SecureValueList{}, resource.WithKind("SecureValue"), + resource.WithPlural("securevalues"), resource.WithScope(resource.NamespacedScope)) + kindSecureValue = resource.Kind{ + Schema: schemaSecureValue, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &SecureValueJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func SecureValueKind() resource.Kind { + return kindSecureValue +} + +// Schema returns a resource.SimpleSchema representation of SecureValue +func SecureValueSchema() *resource.SimpleSchema { + return schemaSecureValue +} + +// Interface compliance checks +var _ resource.Schema = kindSecureValue diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go new file mode 100644 index 00000000000..1b5d0956893 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_spec_gen.go @@ -0,0 +1,46 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// ExposedSecureValue contains the raw decrypted secure value. +// +k8s:openapi-gen=true +type SecureValueExposedSecureValue string + +// +k8s:openapi-gen=true +type SecureValueSpec struct { + // Short description that explains the purpose of this SecureValue. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=25 + Description string `json:"description"` + // The raw value is only valid for write. Read/List will always be empty. + // There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. + // Minimum and maximum lengths in bytes. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=24576 + // +optional + Value *SecureValueExposedSecureValue `json:"value,omitempty"` + // When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. + // This should not contain sensitive information. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=1024 + // +optional + Ref *string `json:"ref,omitempty"` + // Name of the keeper, being the actual storage of the secure value. + // If not specified, the default keeper for the namespace will be used. + // +k8s:validation:minLength=1 + // +k8s:validation:maxLength=253 + // +optional + Keeper *string `json:"keeper,omitempty"` + // The Decrypters that are allowed to decrypt this secret. + // An empty list means no service can decrypt it. + // +k8s:validation:maxItems=64 + // +k8s:validation:uniqueItems=true + // +listType=atomic + // +optional + Decrypters []string `json:"decrypters,omitempty"` +} + +// NewSecureValueSpec creates a new SecureValueSpec object. +func NewSecureValueSpec() *SecureValueSpec { + return &SecureValueSpec{} +} diff --git a/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go new file mode 100644 index 00000000000..eac159c6035 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/securevalue_status_gen.go @@ -0,0 +1,47 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v1beta1 + +// +k8s:openapi-gen=true +type SecureValuestatusOperatorState struct { + // lastEvaluation is the ResourceVersion last evaluated + LastEvaluation string `json:"lastEvaluation"` + // state describes the state of the lastEvaluation. + // It is limited to three possible states for machine evaluation. + State SecureValueStatusOperatorStateState `json:"state"` + // descriptiveState is an optional more descriptive state field which has no requirements on format + DescriptiveState *string `json:"descriptiveState,omitempty"` + // details contains any extra information that is operator-specific + Details map[string]interface{} `json:"details,omitempty"` +} + +// NewSecureValuestatusOperatorState creates a new SecureValuestatusOperatorState object. +func NewSecureValuestatusOperatorState() *SecureValuestatusOperatorState { + return &SecureValuestatusOperatorState{} +} + +// +k8s:openapi-gen=true +type SecureValueStatus struct { + Version int64 `json:"version"` + // operatorStates is a map of operator ID to operator state evaluations. + // Any operator which consumes this kind SHOULD add its state evaluation information to this field. + OperatorStates map[string]SecureValuestatusOperatorState `json:"operatorStates,omitempty"` + // +optional + ExternalID string `json:"externalID"` + // additionalFields is reserved for future use + AdditionalFields map[string]interface{} `json:"additionalFields,omitempty"` +} + +// NewSecureValueStatus creates a new SecureValueStatus object. +func NewSecureValueStatus() *SecureValueStatus { + return &SecureValueStatus{} +} + +// +k8s:openapi-gen=true +type SecureValueStatusOperatorStateState string + +const ( + SecureValueStatusOperatorStateStateSuccess SecureValueStatusOperatorStateState = "success" + SecureValueStatusOperatorStateStateInProgress SecureValueStatusOperatorStateState = "in_progress" + SecureValueStatusOperatorStateStateFailed SecureValueStatusOperatorStateState = "failed" +) diff --git a/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go new file mode 100644 index 00000000000..87ba7504932 --- /dev/null +++ b/apps/secret/pkg/apis/secret/v1beta1/zz_openapi_gen.go @@ -0,0 +1,736 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Code generated by grafana-app-sdk. DO NOT EDIT. + +package v1beta1 + +import ( + common "k8s.io/kube-openapi/pkg/common" + spec "k8s.io/kube-openapi/pkg/validation/spec" + ptr "k8s.io/utils/ptr" +) + +func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper": schema_pkg_apis_secret_v1beta1_Keeper(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig": schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig": schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue": schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig": schema_pkg_apis_secret_v1beta1_KeeperGCPConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig": schema_pkg_apis_secret_v1beta1_KeeperHashiCorpConfig(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperList": schema_pkg_apis_secret_v1beta1_KeeperList(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec": schema_pkg_apis_secret_v1beta1_KeeperSpec(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus": schema_pkg_apis_secret_v1beta1_KeeperStatus(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState": schema_pkg_apis_secret_v1beta1_KeeperstatusOperatorState(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue": schema_pkg_apis_secret_v1beta1_SecureValue(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueList": schema_pkg_apis_secret_v1beta1_SecureValueList(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec": schema_pkg_apis_secret_v1beta1_SecureValueSpec(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus": schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref), + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState": schema_pkg_apis_secret_v1beta1_SecureValuestatusOperatorState(ref), + } +} + +func schema_pkg_apis_secret_v1beta1_Keeper(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Keeper", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperSpec", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperAWSConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "accessKeyID": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), + }, + }, + "secretAccessKey": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), + }, + }, + "kmsKeyID": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"accessKeyID", "secretAccessKey"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperAzureConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "keyVaultName": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "tenantID": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "clientID": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "clientSecret": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), + }, + }, + }, + Required: []string{"keyVaultName", "tenantID", "clientID", "clientSecret"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperCredentialValue(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "secureValueName": { + SchemaProps: spec.SchemaProps{ + Description: "The name of the secure value that holds the actual value.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "valueFromEnv": { + SchemaProps: spec.SchemaProps{ + Description: "The value is taken from the environment variable.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "valueFromConfig": { + SchemaProps: spec.SchemaProps{ + Description: "The value is taken from the Grafana config file.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperGCPConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "projectID": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "credentialsFile": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"projectID", "credentialsFile"}, + }, + }, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperHashiCorpConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "address": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "token": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"), + }, + }, + }, + Required: []string{"address", "token"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperCredentialValue"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.Keeper", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Short description for the Keeper.", + Default: "", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](253), + Type: []string{"string"}, + Format: "", + }, + }, + "aws": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "AWS Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig"), + }, + }, + "azure": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Azure Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig"), + }, + }, + "gcp": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "GCP Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig"), + }, + }, + "hashiCorpVault": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "HashiCorp Vault Keeper Configuration.", + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig"), + }, + }, + }, + Required: []string{"description"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAWSConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperAzureConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperGCPConfig", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperHashiCorpConfig"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState"), + }, + }, + }, + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.KeeperstatusOperatorState"}, + } +} + +func schema_pkg_apis_secret_v1beta1_KeeperstatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the SecureValue", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus"), + }, + }, + }, + Required: []string{"metadata", "spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueSpec", "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValueStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValueList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValue", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValueSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "description": { + SchemaProps: spec.SchemaProps{ + Description: "Short description that explains the purpose of this SecureValue.", + Default: "", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](25), + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. Minimum and maximum lengths in bytes.", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](24576), + Type: []string{"string"}, + Format: "", + }, + }, + "ref": { + SchemaProps: spec.SchemaProps{ + Description: "When using a third-party keeper, the `ref` is used to reference a value inside the remote storage. This should not contain sensitive information.", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](1024), + Type: []string{"string"}, + Format: "", + }, + }, + "keeper": { + SchemaProps: spec.SchemaProps{ + Description: "Name of the keeper, being the actual storage of the secure value. If not specified, the default keeper for the namespace will be used.", + MinLength: ptr.To[int64](1), + MaxLength: ptr.To[int64](253), + Type: []string{"string"}, + Format: "", + }, + }, + "decrypters": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "The Decrypters that are allowed to decrypt this secret. An empty list means no service can decrypt it.", + MaxItems: ptr.To[int64](64), + UniqueItems: true, + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"description"}, + }, + }, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValueStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "version": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "operatorStates": { + SchemaProps: spec.SchemaProps{ + Description: "operatorStates is a map of operator ID to operator state evaluations. Any operator which consumes this kind SHOULD add its state evaluation information to this field.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState"), + }, + }, + }, + }, + }, + "externalID": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "additionalFields": { + SchemaProps: spec.SchemaProps{ + Description: "additionalFields is reserved for future use", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"version"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1.SecureValuestatusOperatorState"}, + } +} + +func schema_pkg_apis_secret_v1beta1_SecureValuestatusOperatorState(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "lastEvaluation": { + SchemaProps: spec.SchemaProps{ + Description: "lastEvaluation is the ResourceVersion last evaluated", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "state": { + SchemaProps: spec.SchemaProps{ + Description: "state describes the state of the lastEvaluation. It is limited to three possible states for machine evaluation.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "descriptiveState": { + SchemaProps: spec.SchemaProps{ + Description: "descriptiveState is an optional more descriptive state field which has no requirements on format", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "details contains any extra information that is operator-specific", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"lastEvaluation", "state"}, + }, + }, + } +} diff --git a/apps/secret/pkg/apis/secret_manifest.go b/apps/secret/pkg/apis/secret_manifest.go new file mode 100644 index 00000000000..12138bbf57b --- /dev/null +++ b/apps/secret/pkg/apis/secret_manifest.go @@ -0,0 +1,65 @@ +// +// This file is generated by grafana-app-sdk +// DO NOT EDIT +// + +package apis + +import ( + "fmt" + + "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/resource" + + v1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" +) + +var () + +var appManifestData = app.ManifestData{ + AppName: "secret", + Group: "secret.grafana.app", + Kinds: []app.ManifestKind{ + { + Kind: "SecureValue", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v1beta1", + }, + }, + }, + + { + Kind: "Keeper", + Scope: "Namespaced", + Conversion: false, + Versions: []app.ManifestKindVersion{ + { + Name: "v1beta1", + }, + }, + }, + }, +} + +func LocalManifest() app.Manifest { + return app.NewEmbeddedManifest(appManifestData) +} + +func RemoteManifest() app.Manifest { + return app.NewAPIServerManifest("secret") +} + +var kindVersionToGoType = map[string]resource.Kind{ + "SecureValue/v1beta1": v1beta1.SecureValueKind(), + "Keeper/v1beta1": v1beta1.KeeperKind(), +} + +// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists. +// If there is no association for the provided Kind and Version, exists will return false. +func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exists bool) { + goType, exists = kindVersionToGoType[fmt.Sprintf("%s/%s", kind, version)] + return goType, exists +} diff --git a/go.work b/go.work index 044c12b9a93..9ea7a9d70c7 100644 --- a/go.work +++ b/go.work @@ -12,6 +12,7 @@ use ( ./apps/iam ./apps/investigations ./apps/playlist + ./apps/secret ./pkg/aggregator ./pkg/apimachinery ./pkg/apis/secret // @grafana/grafana-operator-experience-squad diff --git a/go.work.sum b/go.work.sum index f371854b77f..a2b8c460b40 100644 --- a/go.work.sum +++ b/go.work.sum @@ -608,10 +608,13 @@ github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1: github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8= @@ -645,6 +648,7 @@ github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDz github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= @@ -714,6 +718,7 @@ github.com/grafana/grafana/apps/dashboard v0.0.0-20250616145019-8d27f12428cb/go. github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d/go.mod h1:HQprw3MmiYj5OUV9CZnkwA1FKDZBmYACuAB3oDvUOmI= github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d/go.mod h1:DjJe5osrW/BKrzN9hAAOSElNWutj1bcriExa7iDP7kA= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250121113133-e747350fee2d/go.mod h1:1sq0guad+G4SUTlBgx7SXfhnzy7D86K/LcVOtiQCiMA= +github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250710134100-1f3dc0533caf/go.mod h1:eAlOam2uWhrsEZlOoAr7XZ9hbBP7SyYGYn31/aQAPs8= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d/go.mod h1:CXpwZ3Mkw6xVlGKc0SqUxqXCP3Uv182q6qAQnLaLxRg= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:xrKQcxQxz+IUF90ybtfENFeEXtlj9nAsX/3Fw0KEIeQ= @@ -733,6 +738,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -893,7 +900,9 @@ github.com/ncw/swift/v2 v2.0.2/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItc 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/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1 h1:+aiMrDR6xiaDM7xN4ByrBYI0Craqt68nZicmpYpt0co= github.com/open-telemetry/opentelemetry-collector-contrib/exporter/zipkinexporter v0.124.1/go.mod h1:H/TEWN4jgExt0McrtrBK2VFK6r9LRsWtqhEZrH690rs= github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.124.1 h1:NrjsoVPxI6lmV8jPImDcMeqYh+97Y71f/HB5Sfpfe3I= @@ -1239,12 +1248,22 @@ go.opentelemetry.io/otel/bridge/opencensus v1.35.0 h1:4nJfffRbozhqnuukfRkiahA94m go.opentelemetry.io/otel/bridge/opencensus v1.35.0/go.mod h1:359S30saRYNsB4A46EDx91SpXsQFNgkma7ftg2/L5/M= go.opentelemetry.io/otel/bridge/opentracing v1.35.0 h1:qT4jl1fYl0hHuRopNcwS94QosLFhGYcS0HacPUeXmT4= go.opentelemetry.io/otel/bridge/opentracing v1.35.0/go.mod h1:p5CbIL4v7uQz7mnQD6T/AZc1pPUzwz+2wZ1zrGY9Kgs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= @@ -1252,6 +1271,8 @@ go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+M go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= @@ -1275,6 +1296,7 @@ golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= @@ -1290,6 +1312,7 @@ golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -1302,6 +1325,8 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= @@ -1336,6 +1361,9 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go. google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg= google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2 h1:DbpkGFGRkd4GORg+IWQW2EhxUaa/My/PM8d1CGyTDMY= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= @@ -1353,15 +1381,20 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s= google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= @@ -1386,6 +1419,7 @@ k8s.io/code-generator v0.33.1/go.mod h1:HUKT7Ubp6bOgIbbaPIs9lpd2Q02uqkMCMx9/GjDr k8s.io/code-generator v0.33.2 h1:PCJ0Y6viTCxxJHMOyGqYwWEteM4q6y1Hqo2rNpl6jF4= k8s.io/code-generator v0.33.2/go.mod h1:hBjCA9kPMpjLWwxcr75ReaQfFXY8u+9bEJJ7kRw3J8c= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6 h1:4s3/R4+OYYYUKptXPhZKjQ04WJ6EhQQVFdjOFvCazDk= +k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog= k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= @@ -1406,4 +1440,5 @@ rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= From ce73e5126ac040707f97cf2a4905f5bfb3615145 Mon Sep 17 00:00:00 2001 From: maicon Date: Fri, 11 Jul 2025 09:20:01 -0300 Subject: [PATCH 16/25] Folders/k8s: Enable all dualwriter modes on remaining integration tests (#107992) Signed-off-by: Maicon Costa --- pkg/api/apierrors/folder.go | 3 +- pkg/registry/apis/folders/folder_storage.go | 4 +- pkg/registry/apis/folders/register.go | 9 + pkg/tests/apis/folder/folders_test.go | 382 ++++++++++---------- 4 files changed, 211 insertions(+), 187 deletions(-) diff --git a/pkg/api/apierrors/folder.go b/pkg/api/apierrors/folder.go index 18ab865426d..7ed21c04fb5 100644 --- a/pkg/api/apierrors/folder.go +++ b/pkg/api/apierrors/folder.go @@ -41,7 +41,8 @@ func ToFolderErrorResponse(err error) response.Response { return response.Error(http.StatusConflict, err.Error(), nil) } - if errors.Is(err, dashboards.ErrFolderVersionMismatch) { + if errors.Is(err, dashboards.ErrFolderVersionMismatch) || + k8sErrors.IsAlreadyExists(err) { return response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}) } diff --git a/pkg/registry/apis/folders/folder_storage.go b/pkg/registry/apis/folders/folder_storage.go index 588681e0311..2f3ab79873e 100644 --- a/pkg/registry/apis/folders/folder_storage.go +++ b/pkg/registry/apis/folders/folder_storage.go @@ -11,6 +11,7 @@ import ( claims "github.com/grafana/authlib/types" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" + "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" @@ -79,7 +80,8 @@ func (s *folderStorage) Create(ctx context.Context, ) (runtime.Object, error) { obj, err := s.store.Create(ctx, obj, createValidation, options) if err != nil { - return nil, err + statusErr := apierrors.ToFolderStatusError(err) + return nil, &statusErr } info, err := request.NamespaceInfoFrom(ctx, true) diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 8407cf348cb..397cbcb94a5 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -34,6 +34,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" + "github.com/grafana/grafana/pkg/util" ) var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil) @@ -302,6 +303,14 @@ func (b *FolderAPIBuilder) validateOnCreate(ctx context.Context, id string, obj } } + if !util.IsValidShortUID(id) { + return dashboards.ErrDashboardInvalidUid + } + + if util.IsShortUIDTooLong(id) { + return dashboards.ErrDashboardUidTooLong + } + f, ok := obj.(*folders.Folder) if !ok { return fmt.Errorf("obj is not folders.Folder") diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 29a5a6f1814..5001e90f42c 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -664,52 +664,56 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { }, } - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + for _, tc := range tcs { + t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesClientDashboardsFolders, + }, + }) - user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) + user := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) - parentPayload := `{ + parentPayload := `{ "title": "Test/parent", "uid": "parentuid" }` - parentCreate := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(parentPayload), - }, &folder.Folder{}) - require.NotNil(t, parentCreate.Result) - parentUID := parentCreate.Result.UID - require.NotEmpty(t, parentUID) + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + parentUID := parentCreate.Result.UID + require.NotEmpty(t, parentUID) - resp := apis.DoRequest(helper, apis.RequestParams{ - User: user, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(tc.input), - }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, resp.Response.StatusCode) + resp := apis.DoRequest(helper, apis.RequestParams{ + User: user, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(tc.input), + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, resp.Response.StatusCode) - if tc.expectedCode == http.StatusOK { - require.Equal(t, "uid", resp.Result.UID) - require.Equal(t, "Folder", resp.Result.Title) - } - }) + if tc.expectedCode == http.StatusOK { + require.Equal(t, "uid", resp.Result.UID) + require.Equal(t, "Folder", resp.Result.Title) + } + }) + } } } @@ -768,100 +772,104 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { }, } - for _, tc := range tcs { - t.Run(tc.description, func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + for _, tc := range tcs { + t.Run(fmt.Sprintf("[Mode: %v] "+tc.description, mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - }) + EnableFeatureToggles: []string{ + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesClientDashboardsFolders, + }, + }) - // Create parent folder - parentPayload := `{ + // Create parent folder + parentPayload := `{ "title": "testparent", "uid": "parentuid" }` - parentCreate := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(parentPayload), - }, &folder.Folder{}) - require.NotNil(t, parentCreate.Result) - parentUID := parentCreate.Result.UID - require.NotEmpty(t, parentUID) + parentCreate := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(parentPayload), + }, &folder.Folder{}) + require.NotNil(t, parentCreate.Result) + parentUID := parentCreate.Result.UID + require.NotEmpty(t, parentUID) - // Create descendant folder - payload := "{ \"uid\": \"descuid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" - resp := apis.DoRequest(helper, apis.RequestParams{ - User: helper.Org1.Admin, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(payload), - }, &dtos.Folder{}) - require.Equal(t, http.StatusOK, resp.Response.StatusCode) - - user := helper.CreateUser("user", apis.Org1, org.RoleNone, tc.permissions) - - // Get with accesscontrol disabled - getResp := apis.DoRequest(helper, apis.RequestParams{ - User: user, - Method: http.MethodGet, - Path: "/api/folders/descuid", - }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, getResp.Response.StatusCode) - require.NotNil(t, getResp.Result) - - require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersRead]) - require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersWrite]) - - parents := getResp.Result.Parents - require.Equal(t, len(tc.expectedParentUIDs), len(parents)) - require.Equal(t, len(tc.expectedParentTitles), len(parents)) - for i := 0; i < len(tc.expectedParentUIDs); i++ { - require.Equal(t, tc.expectedParentUIDs[i], parents[i].UID) - require.Equal(t, tc.expectedParentTitles[i], parents[i].Title) - } - - // Get with accesscontrol enabled - if tc.checkAccessControl { - acPerms := []resourcepermissions.SetResourcePermissionCommand{ - { - Actions: []string{dashboards.ActionFoldersRead}, - Resource: "folders", - ResourceAttribute: "uid", - ResourceID: "*", - }, - { - Actions: []string{dashboards.ActionFoldersWrite}, - Resource: "folders", - ResourceAttribute: "uid", - ResourceID: "parentuid", - }, - } - acUser := helper.CreateUser("acuser", apis.Org1, org.RoleNone, acPerms) - - getWithAC := apis.DoRequest(helper, apis.RequestParams{ - User: acUser, - Method: http.MethodGet, - Path: "/api/folders/descuid?accesscontrol=true", + // Create descendant folder + payload := "{ \"uid\": \"descuid\", \"title\": \"Folder\", \"parentUid\": \"parentuid\"}" + resp := apis.DoRequest(helper, apis.RequestParams{ + User: helper.Org1.Admin, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(payload), }, &dtos.Folder{}) - require.Equal(t, tc.expectedCode, getWithAC.Response.StatusCode) - require.NotNil(t, getWithAC.Result) + require.Equal(t, http.StatusOK, resp.Response.StatusCode) - require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersRead]) - require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersWrite]) - } - }) + user := helper.CreateUser("user", apis.Org1, org.RoleNone, tc.permissions) + + // Get with accesscontrol disabled + getResp := apis.DoRequest(helper, apis.RequestParams{ + User: user, + Method: http.MethodGet, + Path: "/api/folders/descuid", + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, getResp.Response.StatusCode) + require.NotNil(t, getResp.Result) + + require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersRead]) + require.False(t, getResp.Result.AccessControl[dashboards.ActionFoldersWrite]) + + parents := getResp.Result.Parents + require.Equal(t, len(tc.expectedParentUIDs), len(parents)) + require.Equal(t, len(tc.expectedParentTitles), len(parents)) + for i := 0; i < len(tc.expectedParentUIDs); i++ { + require.Equal(t, tc.expectedParentUIDs[i], parents[i].UID) + require.Equal(t, tc.expectedParentTitles[i], parents[i].Title) + } + + // Get with accesscontrol enabled + if tc.checkAccessControl { + acPerms := []resourcepermissions.SetResourcePermissionCommand{ + { + Actions: []string{dashboards.ActionFoldersRead}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: "*", + }, + { + Actions: []string{dashboards.ActionFoldersWrite}, + Resource: "folders", + ResourceAttribute: "uid", + ResourceID: "parentuid", + }, + } + acUser := helper.CreateUser("acuser", apis.Org1, org.RoleNone, acPerms) + + getWithAC := apis.DoRequest(helper, apis.RequestParams{ + User: acUser, + Method: http.MethodGet, + Path: "/api/folders/descuid?accesscontrol=true", + }, &dtos.Folder{}) + require.Equal(t, tc.expectedCode, getWithAC.Response.StatusCode) + require.NotNil(t, getWithAC.Result) + + require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersRead]) + require.True(t, getWithAC.Result.AccessControl[dashboards.ActionFoldersWrite]) + } + }) + } } } @@ -948,76 +956,80 @@ func TestIntegrationFoldersCreateAPIEndpointK8S(t *testing.T) { }, } - for _, tc := range tcs { - t.Run(testDescription(tc.description, tc.expectedFolderSvcError), func(t *testing.T) { - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: true, - APIServerStorageType: "unified", - UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folders.RESOURCEGROUP: { - DualWriterMode: grafanarest.Mode1, + // test on all dualwriter modes + for mode := 0; mode <= 4; mode++ { + for _, tc := range tcs { + t.Run(fmt.Sprintf("[Mode: %v] "+testDescription(tc.description, tc.expectedFolderSvcError), mode), func(t *testing.T) { + modeDw := grafanarest.DualWriterMode(mode) + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerStorageType: "unified", + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + folders.RESOURCEGROUP: { + DualWriterMode: modeDw, + }, + }, + EnableFeatureToggles: []string{ + featuremgmt.FlagNestedFolders, + featuremgmt.FlagKubernetesClientDashboardsFolders, }, - }, - EnableFeatureToggles: []string{ - featuremgmt.FlagNestedFolders, - featuremgmt.FlagKubernetesClientDashboardsFolders, - }, - }) - - userTest := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) - - if tc.createSecondRecord { - client := helper.GetResourceClient(apis.ResourceClientArgs{ - User: helper.Org1.Admin, - GVR: gvr, }) - create2 := apis.DoRequest(helper, apis.RequestParams{ - User: client.Args.User, - Method: http.MethodPost, - Path: "/api/folders", - Body: []byte(tc.input), - }, &folder.Folder{}) - require.NotEmpty(t, create2.Response) - require.Equal(t, http.StatusOK, create2.Response.StatusCode) - } - addr := helper.GetEnv().Server.HTTPServer.Listener.Addr() - login := userTest.Identity.GetLogin() - baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password("user"), addr) + userTest := helper.CreateUser("user", apis.Org1, org.RoleViewer, tc.permissions) - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf( - "%s%s", - baseUrl, - "/api/folders", - ), bytes.NewBuffer([]byte(tc.input))) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") + if tc.createSecondRecord { + client := helper.GetResourceClient(apis.ResourceClientArgs{ + User: helper.Org1.Admin, + GVR: gvr, + }) + create2 := apis.DoRequest(helper, apis.RequestParams{ + User: client.Args.User, + Method: http.MethodPost, + Path: "/api/folders", + Body: []byte(tc.input), + }, &folder.Folder{}) + require.NotEmpty(t, create2.Response) + require.Equal(t, http.StatusOK, create2.Response.StatusCode) + } - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - require.NotNil(t, resp) - require.Equal(t, tc.expectedCode, resp.StatusCode) + addr := helper.GetEnv().Server.HTTPServer.Listener.Addr() + login := userTest.Identity.GetLogin() + baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password("user"), addr) - type folderWithMessage struct { - dtos.Folder - Message string `json:"message"` - } + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf( + "%s%s", + baseUrl, + "/api/folders", + ), bytes.NewBuffer([]byte(tc.input))) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") - folder := folderWithMessage{} - err = json.NewDecoder(resp.Body).Decode(&folder) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, tc.expectedCode, resp.StatusCode) - if tc.expectedCode == http.StatusOK { - require.Equal(t, "uid", folder.UID) - require.Equal(t, "Folder", folder.Title) - } + type folderWithMessage struct { + dtos.Folder + Message string `json:"message"` + } - if tc.expectedMessage != "" { - require.Equal(t, tc.expectedMessage, folder.Message) - } - }) + folder := folderWithMessage{} + err = json.NewDecoder(resp.Body).Decode(&folder) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + if tc.expectedCode == http.StatusOK { + require.Equal(t, "uid", folder.UID) + require.Equal(t, "Folder", folder.Title) + } + + if tc.expectedMessage != "" { + require.Equal(t, tc.expectedMessage, folder.Message) + } + }) + } } } From f94722e1e349a2354a29e482a4b376ae03503e09 Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Fri, 11 Jul 2025 14:22:50 +0200 Subject: [PATCH 17/25] Tempo: Add Victoria Logs support for "traces to logs" (#105985) * feat(trace-to-logs): add VictoriaLogs datasource support Signed-off-by: Yury Molodov * tempo: fix lint errors in createSpanLink.tsx Signed-off-by: Yury Molodov --------- Signed-off-by: Yury Molodov --- .../src/TraceToLogs/TraceToLogsSettings.tsx | 1 + .../explore/TraceView/createSpanLink.test.ts | 85 +++++++++++++++++++ .../explore/TraceView/createSpanLink.tsx | 46 ++++++++++ 3 files changed, 132 insertions(+) diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx index b5bca15f317..50870a2d5bc 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx @@ -80,6 +80,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { 'grafana-opensearch-datasource', // external 'grafana-falconlogscale-datasource', // external 'googlecloud-logging-datasource', // external + 'victoriametrics-logs-datasource', // external ]; const traceToLogs = useMemo( diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 03a4651e8a5..7574434861c 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -1515,6 +1515,91 @@ describe('createSpanLinkFactory', () => { expect(decodeURIComponent(links![0].href)).toContain('spanName=\\"operation\\"'); }); }); + + describe('should return victorialogs link', () => { + const victoriaLogsUID = 'victoriaLogsUID'; + + beforeAll(() => { + setDataSourceSrv({ + getInstanceSettings() { + return { + uid: victoriaLogsUID, + name: 'VictoriaLogs', + type: 'victoriametrics-logs-datasource', + } as unknown as DataSourceInstanceSettings; + }, + } as unknown as DataSourceSrv); + + setLinkSrv(new LinkSrv()); + setTemplateSrv(new TemplateSrv()); + }); + + it('with default keys when tags not configured', () => { + const createLink = setupSpanLinkFactory({}, victoriaLogsUID); + const links = createLink!(createTraceSpan()); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"cluster:=\\"cluster1\\" AND hostname:=\\"hostname1\\" AND service_namespace:=\\"namespace1\\"","refId":""}]}' + )}` + ); + }); + + it('formats query correctly if filterByTraceID and filterBySpanID is true', () => { + const createLink = setupSpanLinkFactory( + { + datasourceUid: victoriaLogsUID, + filterByTraceID: true, + filterBySpanID: true, + }, + victoriaLogsUID + ); + + const links = createLink!(createTraceSpan()); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"span_id:=\\"6605c7b08e715d6c\\" AND trace_id:=\\"7946b05c2e2e4e5a\\" AND cluster:=\\"cluster1\\" AND hostname:=\\"hostname1\\" AND service_namespace:=\\"namespace1\\"","refId":""}]}' + )}` + ); + }); + + it('should format multiple tags correctly', () => { + const createLink = setupSpanLinkFactory( + { + tags: [{ key: 'ip' }, { key: 'hostname' }], + }, + victoriaLogsUID + ); + + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'hostname', value: 'hostname1' }, + { key: 'ip', value: '192.168.0.1' }, + ], + }, + }) + ); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"hostname:=\\"hostname1\\" AND ip:=\\"192.168.0.1\\"","refId":""}]}' + )}` + ); + }); + }); }); describe('dataFrame links', () => { diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index ee689c39920..68b2dba6485 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -196,6 +196,13 @@ function legacyCreateSpanLinkFactory( case 'googlecloud-logging-datasource': tags = getFormattedTags(span, tagsToUse, { joinBy: ' AND ' }); query = getQueryForGoogleCloudLogging(span, traceToLogsOptions, tags, customQuery); + break; + case 'victoriametrics-logs-datasource': + // Build tag selector using strict equality (":=") required by LogsQL + // See https://docs.victoriametrics.com/victorialogs/logsql/#exact-filter + tags = getFormattedTags(span, tagsToUse, { labelValueSign: ':=', joinBy: ' AND ' }); + query = getQueryForVictoriaLogs(span, traceToLogsOptions, tags, customQuery); + break; } // query can be false in case the simple UI tag mapping is used but none of them are present in the span. @@ -568,6 +575,45 @@ function getQueryForFalconLogScale(span: TraceSpan, options: TraceToLogsOptionsV }; } +/** + * Builds a LogsQL expression for victoria‑metrics‑logs‑datasource. + * Uses := for exact‑match filters and joins parts with AND. + * See https://docs.victoriametrics.com/victorialogs/logsql/#exact-filter + */ +function getQueryForVictoriaLogs(span: TraceSpan, options: TraceToLogsOptionsV2, tags: string, customQuery?: string) { + const { filterByTraceID, filterBySpanID } = options; + + // Custom user query has priority + if (customQuery) { + return { + expr: customQuery, + refId: '', + }; + } + + const parts: string[] = []; + + if (filterBySpanID && span.spanID) { + parts.push('span_id:="${__span.spanId}"'); + } + if (filterByTraceID && span.traceID) { + parts.push('trace_id:="${__span.traceId}"'); + } + if (tags) { + parts.push('${__tags}'); + } + + // Nothing to match against – do not create the link + if (!parts.length) { + return undefined; + } + + return { + expr: parts.join(' AND '), + refId: '', + }; +} + /** * Creates a string representing all the tags already formatted for use in the query. The tags are filtered so that * only intersection of tags that exist in a span and tags that you want are serialized into the string. From 39d7fbd66e3c020b16802fe6f26efaae8474d8dc Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Fri, 11 Jul 2025 07:25:43 -0500 Subject: [PATCH 18/25] Docs: Updating team sync admonition wording (#107990) --- .../configure-authentication/auth-proxy/index.md | 2 +- .../configure-authentication/azuread/index.md | 2 +- .../configure-authentication/generic-oauth/index.md | 2 +- .../configure-security/configure-authentication/github/index.md | 2 +- .../configure-security/configure-authentication/gitlab/index.md | 2 +- .../configure-security/configure-authentication/google/index.md | 2 +- .../configure-authentication/keycloak/index.md | 2 +- .../configure-security/configure-authentication/okta/index.md | 2 +- .../saml/configure-saml-team-role-mapping/_index.md | 2 +- .../setup-grafana/configure-security/configure-team-sync.md | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md index cee41701c3f..f381c212abb 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md @@ -239,7 +239,7 @@ If the user is deleted from Grafana, the user will be not be able to login and r ### Team Sync {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} With Team Sync, it's possible to set up synchronization between teams in your authentication provider and Grafana. You can send Grafana values as part of an HTTP header and have Grafana map them to your team structure. This allows you to put users into specific teams automatically. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md index 2405baa4ca0..e43a45b88d1 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/azuread/index.md @@ -421,7 +421,7 @@ auto_login = true ### Team Sync {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} With Team Sync you can map your Entra ID groups to teams in Grafana so that your users will automatically be added to diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index d1a613d6b66..7b8d312b4af 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -320,7 +320,7 @@ org_mapping = org_foo:org_foo:Viewer org_bar:org_bar:Editor *:org_baz:Editor ## Configure team synchronization {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can link your OAuth2 groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index 86edef93132..9546be11af3 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -211,7 +211,7 @@ role_attribute_path = [login=='octocat'][0] && 'GrafanaAdmin' || 'Viewer' ## Configure team synchronization {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can map teams from your GitHub organization to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 8f0aaf0f38f..16c3ba72032 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -237,7 +237,7 @@ use_refresh_token = true ## Configure team synchronization {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md index b162eb52305..8085e222252 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/google/index.md @@ -160,7 +160,7 @@ auto_login = true ### Configure team synchronization {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} With team sync, you can easily add users to teams by utilizing their Google groups. To set up team sync for Google OAuth, refer to the following example. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md index f2107a4bff3..88adc9d4a2a 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/keycloak/index.md @@ -109,7 +109,7 @@ viewer ## Team sync {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} [Teamsync](https://grafana.com/docs/grafana//setup-grafana/configure-security/configure-team-sync/) is a feature that allows you to map groups from your identity provider to Grafana teams. This is useful if you want to give your users access to specific dashboards or folders based on their group membership. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md index 93a21c70bc1..3a90f19b371 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/okta/index.md @@ -238,7 +238,7 @@ org_mapping = ["Group 1:org_foo:Viewer", "Group 2:org_bar:Editor", "*:3:Editor"] ### Configure team synchronization {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} By using Team Sync, you can link your Okta groups to teams within Grafana. This will automatically assign users to the appropriate teams. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md index 9a021e7413c..edfedcefb9a 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/configure-saml-team-role-mapping/_index.md @@ -12,7 +12,7 @@ weight: 540 # Configure team sync for SAML {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} To use SAML Team sync, set [`assertion_attribute_groups`](https://grafana.com/docs/grafana//setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_groups) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index dd19cf8f0b5..f99de6a6189 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -17,7 +17,7 @@ weight: 1000 Team sync lets you set up synchronization between your auth providers teams and teams in Grafana. This enables LDAP, OAuth, or SAML users who are members of certain teams or groups to automatically be added or removed as members of certain teams in Grafana. {{< admonition type="note" >}} -Only available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). +Available in [Grafana Enterprise](https://grafana.com/docs/grafana//introduction/grafana-enterprise/) and [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/). {{< /admonition >}} Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized in the team members list, see `LDAP` label in screenshot. From 9d0a23e1f5d138afaf087a115183cf7501c0fe6c Mon Sep 17 00:00:00 2001 From: Bruno Date: Fri, 11 Jul 2025 09:40:50 -0300 Subject: [PATCH 19/25] Secrets: add crudl+decrypt state machine test (#107971) * Secrets: add state machine test for CRUDL+decrpt operations * make update-workspace * make update-workspace * make enterprise-dev * make update-workspace * fix go.mod * make update-workspace * fix gomod * make update-workspace --------- Co-authored-by: Matheus Macabu --- go.mod | 1 + go.sum | 2 + go.work.sum | 2 + .../apis/secret/service/secure_value.go | 1 + .../apis/secret/testutils/testutils.go | 52 ++- .../secret/metadata/secure_value_test.go | 419 ++++++++++++++++++ 6 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 pkg/storage/secret/metadata/secure_value_test.go diff --git a/go.mod b/go.mod index 3096b0ef889..dfb884a040c 100644 --- a/go.mod +++ b/go.mod @@ -219,6 +219,7 @@ require ( k8s.io/kube-aggregator v0.33.1 // @grafana/grafana-app-platform-squad k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // @grafana/grafana-app-platform-squad k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // @grafana/partner-datasources + pgregory.net/rapid v1.2.0 // @grafana/grafana-operator-experience-squad sigs.k8s.io/randfill v1.0.0 // @grafana/grafana-app-platform-squad sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // @grafana-app-platform-squad xorm.io/builder v0.3.6 // @grafana/grafana-backend-group diff --git a/go.sum b/go.sum index c9040b526ff..0a022fc6562 100644 --- a/go.sum +++ b/go.sum @@ -3632,6 +3632,8 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/go.work.sum b/go.work.sum index a2b8c460b40..754974ef77b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1434,6 +1434,8 @@ modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJ modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= diff --git a/pkg/registry/apis/secret/service/secure_value.go b/pkg/registry/apis/secret/service/secure_value.go index 01b382edbf7..2611eb5a41b 100644 --- a/pkg/registry/apis/secret/service/secure_value.go +++ b/pkg/registry/apis/secret/service/secure_value.go @@ -194,6 +194,7 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa )) defer span.End() + // TODO: does this need to be for update? sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true}) if err != nil { return nil, fmt.Errorf("fetching secure value: %+w", err) diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 281304320c8..b85b098d834 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -4,6 +4,9 @@ import ( "context" "testing" + "github.com/grafana/authlib/authn" + "github.com/grafana/authlib/types" + "github.com/grafana/grafana/pkg/apimachinery/identity" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" "go.opentelemetry.io/otel/trace/noop" @@ -70,7 +73,10 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { // Initialize access client + access control accessControl := &actest.FakeAccessControl{ExpectedEvaluate: true} - accessClient := accesscontrol.NewLegacyAccessClient(accessControl) + accessClient := accesscontrol.NewLegacyAccessClient(accessControl, accesscontrol.ResourceAuthorizerOptions{ + Resource: "securevalues", + Attr: "uid", + }) defaultKey := "SdlklWklckeLS" cfg := &setting.Cfg{ @@ -112,13 +118,22 @@ func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut { decryptStorage, err := metadata.ProvideDecryptStorage(features, tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil) require.NoError(t, err) - return Sut{SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, Database: database, DecryptStorage: decryptStorage} + decryptService := decrypt.ProvideDecryptService(decryptStorage) + + return Sut{ + SecureValueService: secureValueService, + SecureValueMetadataStorage: secureValueMetadataStorage, + Database: database, + DecryptStorage: decryptStorage, + DecryptService: decryptService, + } } type Sut struct { SecureValueService *service.SecureValueService SecureValueMetadataStorage contracts.SecureValueMetadataStorage DecryptStorage contracts.DecryptStorage + DecryptService service.DecryptService Database *database.Database } @@ -142,6 +157,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec Spec: secretv0alpha1.SecureValueSpec{ Description: "desc1", Value: secretv0alpha1.NewExposedSecureValue("v1"), + Decrypters: []string{"decrypter1"}, }, Status: secretv0alpha1.SecureValueStatus{}, }, @@ -150,7 +166,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec opt(&cfg) } - createdSv, err := s.SecureValueService.Create(ctx, cfg.Sv, "actor") + createdSv, err := s.SecureValueService.Create(ctx, cfg.Sv, "actor-uid") if err != nil { return nil, err } @@ -158,7 +174,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec } func (s *Sut) UpdateSv(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) { - newSv, _, err := s.SecureValueService.Update(ctx, sv, "actor") + newSv, _, err := s.SecureValueService.Update(ctx, sv, "actor-uid") return newSv, err } @@ -178,3 +194,31 @@ func newKeeperServiceWrapper(keeper contracts.Keeper) *keeperServiceWrapper { func (wrapper *keeperServiceWrapper) KeeperForConfig(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) { return wrapper.keeper, nil } + +func CreateUserAuthContext(ctx context.Context, namespace string, permissions map[string][]string) context.Context { + orgID := int64(1) + requester := &identity.StaticRequester{ + Namespace: namespace, + Type: types.TypeUser, + UserID: 1, + OrgID: orgID, + Permissions: map[int64]map[string][]string{ + orgID: permissions, + }, + } + + return types.WithAuthInfo(ctx, requester) +} + +func CreateServiceAuthContext(ctx context.Context, serviceIdentity string, permissions []string) context.Context { + requester := &identity.StaticRequester{ + AccessTokenClaims: &authn.Claims[authn.AccessTokenClaims]{ + Rest: authn.AccessTokenClaims{ + Permissions: permissions, + ServiceIdentity: serviceIdentity, + }, + }, + } + + return types.WithAuthInfo(ctx, requester) +} diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go new file mode 100644 index 00000000000..459db29f74a --- /dev/null +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -0,0 +1,419 @@ +package metadata_test + +import ( + "fmt" + "slices" + "testing" + + secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" + "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" + "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "pgregory.net/rapid" +) + +type modelSecureValue struct { + *secretv0alpha1.SecureValue + active bool +} + +// A simplified model of the grafana secrets manager +type model struct { + secureValues []*modelSecureValue +} + +func newModel() *model { + return &model{} +} + +func (m *model) getNewVersionNumber(namespace, name string) int64 { + latestVersion := int64(0) + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name { + latestVersion = max(latestVersion, sv.Status.Version) + } + } + return latestVersion + 1 +} + +func (m *model) setVersionToActive(namespace, name string, version int64) { + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name { + sv.active = sv.Status.Version == version + } + } +} + +func (m *model) setVersionToInactive(namespace, name string, version int64) { + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name && sv.Status.Version == version { + sv.active = false + return + } + } +} + +func (m *model) readActiveVersion(namespace, name string) *modelSecureValue { + for _, sv := range m.secureValues { + if sv.Namespace == namespace && sv.Name == name && sv.active { + return sv + } + } + + return nil +} + +func (m *model) create(sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) { + modelSv := &modelSecureValue{sv, false} + modelSv.Status.Version = m.getNewVersionNumber(modelSv.Namespace, modelSv.Name) + modelSv.Status.ExternalID = fmt.Sprintf("%d", modelSv.Status.Version) + m.secureValues = append(m.secureValues, modelSv) + m.setVersionToActive(modelSv.Namespace, modelSv.Name, modelSv.Status.Version) + return modelSv.SecureValue, nil +} + +func (m *model) update(newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, bool, error) { + // If the payload doesn't contain a value, get the value from current version + if newSecureValue.Spec.Value == "" { + sv := m.readActiveVersion(newSecureValue.Namespace, newSecureValue.Name) + if sv == nil { + return nil, false, contracts.ErrSecureValueNotFound + } + newSecureValue.Spec.Value = sv.Spec.Value + } + createdSv, err := m.create(newSecureValue, actorUID) + return createdSv, true, err +} + +func (m *model) delete(namespace, name string) (*secretv0alpha1.SecureValue, error) { + modelSv := m.readActiveVersion(namespace, name) + if modelSv == nil { + return nil, contracts.ErrSecureValueNotFound + } + m.setVersionToInactive(namespace, name, modelSv.Status.Version) + return modelSv.SecureValue, nil +} + +func (m *model) list(namespace string) (*secretv0alpha1.SecureValueList, error) { + out := make([]secretv0alpha1.SecureValue, 0) + + for _, v := range m.secureValues { + if v.Namespace == namespace && v.active { + out = append(out, *v.SecureValue) + } + } + + return &secretv0alpha1.SecureValueList{Items: out}, nil +} + +func (m *model) decrypt(decrypter, namespace, name string) (map[string]service.DecryptResult, error) { + for _, v := range m.secureValues { + if v.Namespace == namespace && + v.Name == name && + v.active { + if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) { + return map[string]service.DecryptResult{ + name: service.NewDecryptResultValue(&v.DeepCopy().Spec.Value), + }, nil + } + + return map[string]service.DecryptResult{ + name: service.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized), + }, nil + } + } + return map[string]service.DecryptResult{ + name: service.NewDecryptResultErr(contracts.ErrDecryptNotFound), + }, nil +} + +func (m *model) read(namespace, name string) (*secretv0alpha1.SecureValue, error) { + modelSv := m.readActiveVersion(namespace, name) + if modelSv == nil { + return nil, contracts.ErrSecureValueNotFound + } + return modelSv.SecureValue, nil +} + +var ( + decryptersGen = rapid.SampledFrom([]string{"svc1", "svc2", "svc3", "svc4", "svc5"}) + nameGen = rapid.SampledFrom([]string{"n1", "n2", "n3", "n4", "n5"}) + namespaceGen = rapid.SampledFrom([]string{"ns1", "ns2", "ns3", "ns4", "ns5"}) + anySecureValueGen = rapid.Custom(func(t *rapid.T) *secretv0alpha1.SecureValue { + return &secretv0alpha1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: nameGen.Draw(t, "name"), + Namespace: namespaceGen.Draw(t, "ns"), + }, + Spec: secretv0alpha1.SecureValueSpec{ + Description: rapid.SampledFrom([]string{"d1", "d2", "d3", "d4", "d5"}).Draw(t, "description"), + Value: secretv0alpha1.NewExposedSecureValue(rapid.SampledFrom([]string{"v1", "v2", "v3", "v4", "v5"}).Draw(t, "value")), + Decrypters: rapid.SliceOfDistinct(decryptersGen, func(v string) string { return v }).Draw(t, "decrypters"), + }, + Status: secretv0alpha1.SecureValueStatus{}, + } + }) + updateSecureValueGen = rapid.Custom(func(t *rapid.T) *secretv0alpha1.SecureValue { + sv := anySecureValueGen.Draw(t, "sv") + // Maybe update the secret value, maybe not + if !rapid.Bool().Draw(t, "should_update_value") { + sv.Spec.Value = "" + } + return sv + }) + // Any secure value will do + deleteSecureValueGen = anySecureValueGen + decryptGen = rapid.Custom(func(t *rapid.T) decryptInput { + return decryptInput{ + namespace: namespaceGen.Draw(t, "ns"), + name: nameGen.Draw(t, "name"), + decrypter: decryptersGen.Draw(t, "decrypter"), + } + }) +) + +type decryptInput struct { + namespace string + name string + decrypter string +} + +func TestModel(t *testing.T) { + t.Parallel() + + sv := &secretv0alpha1.SecureValue{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sv1", + Namespace: "ns1", + }, + Spec: secretv0alpha1.SecureValueSpec{ + Description: "desc1", + Value: secretv0alpha1.NewExposedSecureValue("v1"), + Decrypters: []string{"decrypter1"}, + }, + Status: secretv0alpha1.SecureValueStatus{}, + } + + t.Run("creating secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // Create a secure value + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv1.Namespace) + require.Equal(t, sv.Name, sv1.Name) + require.EqualValues(t, 1, sv1.Status.Version) + + // Create a new version of a secure value + sv2, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv2.Namespace) + require.Equal(t, sv.Name, sv2.Name) + require.EqualValues(t, 2, sv2.Status.Version) + }) + + t.Run("updating secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // Create a new version of a secure value by updating it + sv2, _, err := m.update(sv1.DeepCopy(), "actor-uid") + require.NoError(t, err) + require.Equal(t, sv.Namespace, sv2.Namespace) + require.Equal(t, sv.Name, sv2.Name) + require.EqualValues(t, 2, sv2.Status.Version) + + // Try updating a secure value that doesn't exist without specifying a value for it + sv3 := sv2.DeepCopy() + sv3.Name = "i_dont_exist" + sv3.Spec.Value = "" + _, _, err = m.update(sv3, "actor-uid") + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) + + // Updating a value that doesn't exist creates a new version + sv4 := sv3.DeepCopy() + sv4.Name = "i_dont_exist" + sv4.Spec.Value = secretv0alpha1.NewExposedSecureValue("sv4") + sv4, _, err = m.update(sv4, "actor-uid") + require.NoError(t, err) + require.EqualValues(t, 1, sv4.Status.Version) + }) + + t.Run("deleting a secure value", func(t *testing.T) { + t.Parallel() + + m := newModel() + + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // Deleting a secure value + deletedSv, err := m.delete(sv1.Namespace, sv1.Name) + require.NoError(t, err) + require.Equal(t, sv1.Namespace, deletedSv.Namespace) + require.Equal(t, sv1.Name, deletedSv.Name) + require.EqualValues(t, sv1.Status.Version, deletedSv.Status.Version) + + // Deleting a secure value that doesn't exist results in an error + _, err = m.delete(sv1.Namespace, sv1.Name) + require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) + }) + + t.Run("listing secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // No secure values exist yet + list, err := m.list(sv.Namespace) + require.NoError(t, err) + require.Equal(t, 0, len(list.Items)) + + // Create a secure value + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // 1 secure value exists and it should be returned + list, err = m.list(sv.Namespace) + require.NoError(t, err) + require.Equal(t, 1, len(list.Items)) + require.Equal(t, sv1.Namespace, list.Items[0].Namespace) + require.Equal(t, sv1.Name, list.Items[0].Name) + require.EqualValues(t, sv1.Status.Version, list.Items[0].Status.Version) + }) + + t.Run("decrypting secure values", func(t *testing.T) { + t.Parallel() + + m := newModel() + + // Decrypting a secure value that does not exist + result, err := m.decrypt("decrypter", "namespace", "name") + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Nil(t, result["name"].Value()) + require.ErrorIs(t, result["name"].Error(), contracts.ErrDecryptNotFound) + + // Create a secure value + sv1, err := m.create(sv.DeepCopy(), "actor-uid") + require.NoError(t, err) + + // Decrypt the just created secure value + result, err = m.decrypt(sv1.Spec.Decrypters[0], sv1.Namespace, sv1.Name) + require.NoError(t, err) + require.Equal(t, 1, len(result)) + require.Nil(t, result[sv1.Name].Error()) + require.Equal(t, result[sv1.Name].Value().DangerouslyExposeAndConsumeValue(), sv.DeepCopy().Spec.Value.DangerouslyExposeAndConsumeValue()) + }) +} + +func TestStateMachine(t *testing.T) { + t.Parallel() + + tt := t + + rapid.Check(t, func(t *rapid.T) { + sut := testutils.Setup(tt) + model := newModel() + + t.Repeat(map[string]func(*rapid.T){ + "create": func(t *rapid.T) { + sv := anySecureValueGen.Draw(t, "sv") + modelCreatedSv, modelErr := model.create(sv.DeepCopy(), "actor-uid") + + createdSv, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(sv.DeepCopy())) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelCreatedSv.Namespace, createdSv.Namespace) + require.Equal(t, modelCreatedSv.Name, createdSv.Name) + require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) + }, + "update": func(t *rapid.T) { + sv := updateSecureValueGen.Draw(t, "sv") + modelCreatedSv, _, modelErr := model.update(sv.DeepCopy(), "actor-uid") + createdSv, err := sut.UpdateSv(t.Context(), sv.DeepCopy()) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelCreatedSv.Namespace, createdSv.Namespace) + require.Equal(t, modelCreatedSv.Name, createdSv.Name) + require.Equal(t, modelCreatedSv.Status.Version, createdSv.Status.Version) + }, + "delete": func(t *rapid.T) { + sv := deleteSecureValueGen.Draw(t, "sv") + modelSv, modelErr := model.delete(sv.Namespace, sv.Name) + deletedSv, err := sut.DeleteSv(t.Context(), sv.Namespace, sv.Name) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelSv.Namespace, deletedSv.Namespace) + require.Equal(t, modelSv.Name, deletedSv.Name) + require.Equal(t, modelSv.Status.Version, deletedSv.Status.Version) + }, + "list": func(t *rapid.T) { + sv := anySecureValueGen.Draw(t, "sv") + authCtx := testutils.CreateUserAuthContext(t.Context(), sv.Namespace, map[string][]string{ + "securevalues:read": {"securevalues:uid:*"}, + }) + modelList, modelErr := model.list(sv.Namespace) + list, err := sut.SecureValueService.List(authCtx, xkube.Namespace(sv.Namespace)) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + + require.Equal(t, len(modelList.Items), len(list.Items)) + + // PERFORMANCE: The lists are always small + for _, v1 := range modelList.Items { + if !slices.ContainsFunc(list.Items, func(v2 secretv0alpha1.SecureValue) bool { + return v2.Namespace == v1.Namespace && v2.Name == v1.Name && v2.Status.Version == v1.Status.Version + }) { + t.Fatalf("expected sut to return secure value ns=%+v name=%+v version=%+v in the result", v1.Namespace, v1.Name, v1.Status.Version) + } + } + }, + "get": func(t *rapid.T) { + sv := anySecureValueGen.Draw(t, "sv") + modelSv, modelErr := model.read(sv.Namespace, sv.Name) + readSv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv.Namespace), sv.Name) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + require.Equal(t, modelSv.Namespace, readSv.Namespace) + require.Equal(t, modelSv.Name, readSv.Name) + require.Equal(t, modelSv.Status.Version, readSv.Status.Version) + }, + "decrypt": func(t *rapid.T) { + input := decryptGen.Draw(t, "decryptInput") + authCtx := testutils.CreateServiceAuthContext(t.Context(), input.decrypter, []string{fmt.Sprintf("secret.grafana.app/securevalues/%+v:decrypt", input.name)}) + modelResult, modelErr := model.decrypt(input.decrypter, input.namespace, input.name) + result, err := sut.DecryptService.Decrypt(authCtx, input.namespace, input.name) + if err != nil || modelErr != nil { + require.ErrorIs(t, err, modelErr) + return + } + + require.Equal(t, len(modelResult), len(result)) + for name := range modelResult { + require.Equal(t, modelResult[name].Value(), result[name].Value()) + require.Equal(t, modelResult[name].Error(), result[name].Error()) + } + }, + }) + }) +} From 73ab088804abedc99d8d104017a3f88e47e878db Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Fri, 11 Jul 2025 08:50:25 -0400 Subject: [PATCH 20/25] TableNG: Scrollbar width handling (#107994) * TableNG: Scrollbar width handling * slight cleanup * fix types * fixie --------- Co-authored-by: Leon Sorokin --- .../src/components/Table/TableNG/TableNG.tsx | 11 ++++++++--- .../src/components/Table/TableNG/hooks.ts | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 129eb256c97..dbe5dd3f1a0 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -1,11 +1,12 @@ import 'react-data-grid/lib/styles.css'; import { css, cx } from '@emotion/css'; import { Property } from 'csstype'; -import { Key, ReactNode, useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import { Key, ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Cell, CellRendererProps, DataGrid, + DataGridHandle, DataGridProps, RenderCellProps, RenderRowProps, @@ -39,6 +40,7 @@ import { useHeaderHeight, usePaginatedRows, useRowHeight, + useScrollbarWidth, useSortedRows, useTypographyCtx, } from './hooks'; @@ -151,9 +153,11 @@ export function TableNG(props: TableNGProps) { // vt scrollbar accounting for column auto-sizing const visibleFields = useMemo(() => getVisibleFields(data.fields), [data.fields]); + const gridRef = useRef(null); + const scrollbarWidth = useScrollbarWidth(gridRef, height, sortedRows); const availableWidth = useMemo( - () => (hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width), - [width, hasNestedFrames] + () => (hasNestedFrames ? width - COLUMN.EXPANDER_WIDTH : width) - scrollbarWidth, + [width, hasNestedFrames, scrollbarWidth] ); const typographyCtx = useTypographyCtx(); const widths = useMemo(() => computeColWidths(visibleFields, availableWidth), [visibleFields, availableWidth]); @@ -549,6 +553,7 @@ export function TableNG(props: TableNGProps) { <> {...commonDataGridProps} + ref={gridRef} className={styles.grid} columns={structureRevColumns} rows={paginatedRows} diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index 4ef2c8f185e..c4bcabe0828 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -1,5 +1,5 @@ -import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect } from 'react'; -import { Column, DataGridProps, SortColumn } from 'react-data-grid'; +import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, RefObject } from 'react'; +import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid'; import { varPreLine } from 'uwrap'; import { Field, fieldReducers, FieldType, formattedValueToString, LinkModel, reduceField } from '@grafana/data'; @@ -608,3 +608,17 @@ export function useSingleLink(field: Field, rowIdx: number): LinkModel | undefin const shouldShowLink = linksCount === 1 && actionsCount === 0; return useMemo(() => (shouldShowLink ? (getCellLinks(field, rowIdx) ?? []) : [])[0], [field, shouldShowLink, rowIdx]); } + +export function useScrollbarWidth(ref: RefObject, height: number, renderedRows: TableRow[]) { + const [scrollbarWidth, setScrollbarWidth] = useState(0); + + useLayoutEffect(() => { + const el = ref.current?.element; + + if (el) { + setScrollbarWidth(el.offsetWidth - el.clientWidth); + } + }, [ref, height, renderedRows]); + + return scrollbarWidth; +} From 602b7826c4cc94ff82dc0a63a22adcb7a4247183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 11 Jul 2025 15:33:51 +0200 Subject: [PATCH 21/25] refactor(unified-storage): remove resource_server config option (#107649) --- .../unified/sql/db/dbimpl/db_engine.go | 163 ---------- .../unified/sql/db/dbimpl/db_engine_test.go | 292 ------------------ pkg/storage/unified/sql/db/dbimpl/dbimpl.go | 65 ++-- 3 files changed, 20 insertions(+), 500 deletions(-) diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine.go b/pkg/storage/unified/sql/db/dbimpl/db_engine.go index 9712de24f02..28fe6f9483c 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine.go @@ -1,24 +1,14 @@ package dbimpl import ( - "cmp" "fmt" - "strings" "time" - "github.com/go-sql-driver/mysql" - "github.com/grafana/dskit/crypto/tls" - "github.com/grafana/grafana/pkg/util/xorm" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/storage/unified/sql/db" ) -// tlsConfigName is the name of the TLS config that we register with the MySQL -// driver. -const tlsConfigName = "db_engine_tls" - func getEngine(config *sqlstore.DatabaseConfig) (*xorm.Engine, error) { switch config.Type { case dbTypeMySQL, dbTypePostgres, dbTypeSQLite: @@ -36,156 +26,3 @@ func getEngine(config *sqlstore.DatabaseConfig) (*xorm.Engine, error) { return nil, fmt.Errorf("unsupported database type: %s", config.Type) } } - -// Deprecated: use getEngine instead -func getEngineMySQL(getter confGetter) (*xorm.Engine, error) { - config := mysql.NewConfig() - config.User = getter.String("user") - // accept the core Grafana jargon of `password` as well, originally Unified - // Storage used `pass` - config.Passwd = cmp.Or(getter.String("pass"), getter.String("password")) - config.Net = "tcp" - config.Addr = getter.String("host") - config.DBName = getter.String("name") - config.Params = map[string]string{ - // See: https://dev.mysql.com/doc/refman/en/sql-mode.html - "@@SESSION.sql_mode": "ANSI", - } - config.Collation = "utf8mb4_unicode_ci" - config.Loc = time.UTC - config.AllowNativePasswords = true - config.ClientFoundRows = true - config.ParseTime = true - - // Setup TLS for the database connection if configured. - if err := configureTLS(getter, config); err != nil { - return nil, fmt.Errorf("failed to configure TLS: %w", err) - } - - // allow executing multiple SQL statements in a single roundtrip, and also - // enable executing the CALL statement to run stored procedures that execute - // multiple SQL statements. - //config.MultiStatements = true - - if err := getter.Err(); err != nil { - return nil, fmt.Errorf("config error: %w", err) - } - - if strings.HasPrefix(config.Addr, "/") { - config.Net = "unix" - } - - engine, err := xorm.NewEngine(db.DriverMySQL, config.FormatDSN()) - if err != nil { - return nil, fmt.Errorf("open database: %w", err) - } - - engine.SetMaxOpenConns(getter.Int("max_open_conn", 0)) - engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) - maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second - engine.SetConnMaxLifetime(maxLifetime) - - return engine, nil -} - -func configureTLS(getter confGetter, config *mysql.Config) error { - sslMode := getter.String("ssl_mode") - - if sslMode == "true" || sslMode == "skip-verify" { - tlsCfg := tls.ClientConfig{ - CAPath: getter.String("ca_cert_path"), - CertPath: getter.String("client_cert_path"), - KeyPath: getter.String("client_key_path"), - ServerName: getter.String("server_cert_name"), - } - - rawTLSCfg, err := tlsCfg.GetTLSConfig() - if err != nil { - return fmt.Errorf("failed to get TLS config for mysql: %w", err) - } - - if sslMode == "skip-verify" { - rawTLSCfg.InsecureSkipVerify = true - } - - if err := mysql.RegisterTLSConfig(tlsConfigName, rawTLSCfg); err != nil { - return fmt.Errorf("failed to register TLS config for mysql: %w", err) - } - - config.TLSConfig = tlsConfigName - } - - // If the TLS mode is set in the database config, we need to set it here. - if tls := getter.String("tls"); tls != "" { - // If the user has provided TLS certs, we don't want to use the tls=, as - // they would override the TLS config that we set above. They both use the same - // parameter, so we need to check for that. - if sslMode == "true" { - return fmt.Errorf("cannot provide tls certs and tls= at the same time") - } - config.Params["tls"] = tls - } - - return nil -} - -// Deprecated: use getEngine instead -func getEnginePostgres(getter confGetter) (*xorm.Engine, error) { - dsnKV := map[string]string{ - "user": getter.String("user"), - // accept the core Grafana jargon of `password` as well, originally - // Unified Storage used `pass` - "password": cmp.Or(getter.String("pass"), getter.String("password")), - "dbname": getter.String("name"), - "sslmode": cmp.Or(getter.String("ssl_mode"), "disable"), - "sslsni": getter.String("ssl_sni"), - "sslrootcert": getter.String("ca_cert_path"), - "sslkey": getter.String("client_key_path"), - "sslcert": getter.String("client_cert_path"), - } - - // TODO: probably interesting: - // "passfile", "statement_timeout", "lock_timeout", "connect_timeout" - - // TODO: for CockroachDB, we probably need to use the following: - // dsnKV["options"] = "-c enable_experimental_alter_column_type_general=true" - // Or otherwise specify it as: - // dsnKV["enable_experimental_alter_column_type_general"] = "true" - - // TODO: do we want to support these options in the DSN as well? - // "sslpassword", "krbspn", "krbsrvname", "target_session_attrs", "service", "servicefile" - - // More on Postgres connection string parameters: - // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING - - hostport := getter.String("host") - - if err := getter.Err(); err != nil { - return nil, fmt.Errorf("config error: %w", err) - } - - host, port, err := splitHostPortDefault(hostport, "127.0.0.1", "5432") - if err != nil { - return nil, fmt.Errorf("invalid host: %w", err) - } - dsnKV["host"] = host - dsnKV["port"] = port - - dsn, err := MakeDSN(dsnKV) - if err != nil { - return nil, fmt.Errorf("error building DSN: %w", err) - } - - // FIXME: get rid of xorm - engine, err := xorm.NewEngine(db.DriverPostgres, dsn) - if err != nil { - return nil, fmt.Errorf("open database: %w", err) - } - - engine.SetMaxOpenConns(getter.Int("max_open_conn", 0)) - engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4)) - maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second - engine.SetConnMaxLifetime(maxLifetime) - - return engine, nil -} diff --git a/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go index fbaa147070a..4a889b09ec6 100644 --- a/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go +++ b/pkg/storage/unified/sql/db/dbimpl/db_engine_test.go @@ -1,17 +1,10 @@ package dbimpl import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "math/big" "os" "path/filepath" "strings" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,20 +12,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func newValidMySQLGetter(withKeyPrefix bool) confGetter { - var prefix string - if withKeyPrefix { - prefix = "db_" - } - return newTestConfGetter(map[string]string{ - prefix + "type": dbTypeMySQL, - prefix + "host": "/var/run/mysql.socket", - prefix + "name": "grafana", - prefix + "user": "user", - prefix + "password": "password", - }, prefix) -} - func TestNewResourceDbProvider(t *testing.T) { t.Parallel() @@ -146,274 +125,3 @@ password = password require.Equal(t, dbTypeMySQL, engine.engine.Dialect().DriverName()) require.Contains(t, engine.engine.DataSourceName(), "overthere:3306") } - -func TestGetEngineMySQLFromConfig(t *testing.T) { - t.Parallel() - - t.Run("happy path - with key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(true)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("happy path - without key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEngineMySQL(newValidMySQLGetter(false)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("invalid string", func(t *testing.T) { - t.Parallel() - - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypeMySQL, - "db_host": "/var/run/mysql.socket", - "db_name": string(invalidUTF8ByteSequence), - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEngineMySQL(getter) - require.Nil(t, engine) - require.Error(t, err) - require.ErrorIs(t, err, errInvalidUTF8Sequence) - }) -} - -func newValidPostgresGetter(withKeyPrefix bool) confGetter { - var prefix string - if withKeyPrefix { - prefix = "db_" - } - return newTestConfGetter(map[string]string{ - prefix + "type": dbTypePostgres, - prefix + "host": "localhost", - prefix + "name": "grafana", - prefix + "user": "user", - prefix + "password": "password", - }, prefix) -} - -func TestGetEnginePostgresFromConfig(t *testing.T) { - t.Parallel() - - t.Run("happy path - with key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(true)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("happy path - without key prefix", func(t *testing.T) { - t.Parallel() - engine, err := getEnginePostgres(newValidPostgresGetter(false)) - require.NotNil(t, engine) - require.NoError(t, err) - }) - - t.Run("invalid string", func(t *testing.T) { - t.Parallel() - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypePostgres, - "db_host": string(invalidUTF8ByteSequence), - "db_name": "grafana", - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEnginePostgres(getter) - - require.Nil(t, engine) - require.Error(t, err) - }) - - t.Run("invalid hostport", func(t *testing.T) { - t.Parallel() - getter := newTestConfGetter(map[string]string{ - "db_type": dbTypePostgres, - "db_host": "1:1:1", - "db_name": "grafana", - "db_user": "user", - "db_password": "password", - }, "db_") - engine, err := getEnginePostgres(getter) - - require.Nil(t, engine) - require.Error(t, err) - }) -} - -func TestGetEngineMySQLTLS(t *testing.T) { - certs := generateTestCerts(t) - - tests := []struct { - name string - config map[string]string - shouldErr bool - }{ - { - name: "with TLS disabled", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "disable", - }, - }, - { - name: "with TLS skip-verify", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "skip-verify", - }, - }, - { - name: "with valid TLS certificates", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "true", - "ca_cert_path": certs.caFile, - "client_cert_path": certs.certFile, - "client_key_path": certs.keyFile, - "server_cert_name": "mysql.example.com", - }, - }, - { - name: "with invalid cert paths", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "true", - "ca_cert_path": "nonexistent/ca.pem", - "client_cert_path": "nonexistent/client-cert.pem", - "client_key_path": "nonexistent/client-key.pem", - "server_cert_name": "mysql.example.com", - }, - shouldErr: true, - }, - { - name: "with TLS certs and tls parameter", - config: map[string]string{ - "type": "mysql", - "user": "user", - "pass": "pass", - "host": "localhost", - "name": "dbname", - "ssl_mode": "true", - "ca_cert_path": certs.caFile, - "client_cert_path": certs.certFile, - "client_key_path": certs.keyFile, - "server_cert_name": "mysql.example.com", - "tls": "preferred", - }, - shouldErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - getter := newTestConfGetter(tt.config, "") - engine, err := getEngineMySQL(getter) - - if tt.shouldErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - require.NotNil(t, engine) - }) - } -} - -type testCerts struct { - caFile string - certFile string - keyFile string -} - -func generateTestCerts(t *testing.T) testCerts { - t.Helper() - tempDir := t.TempDir() - - // Generate CA private key - caKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - // Generate CA certificate - ca := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: "Test CA", - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), - IsCA: true, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, - BasicConstraintsValid: true, - } - - caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caKey.PublicKey, caKey) - require.NoError(t, err) - - clientKey, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err) - - client := &x509.Certificate{ - SerialNumber: big.NewInt(2), - Subject: pkix.Name{ - CommonName: "Test Client", - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, - SubjectKeyId: []byte{1, 2, 3, 4, 5}, - } - - clientBytes, err := x509.CreateCertificate(rand.Reader, client, ca, &clientKey.PublicKey, caKey) - require.NoError(t, err) - - // Write certificates and keys to temporary files - caFile := filepath.Join(tempDir, "ca.pem") - certFile := filepath.Join(tempDir, "cert.pem") - keyFile := filepath.Join(tempDir, "key.pem") - - writePEMFile(t, caFile, "CERTIFICATE", caBytes) - writePEMFile(t, certFile, "CERTIFICATE", clientBytes) - writePEMFile(t, keyFile, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(clientKey)) - - return testCerts{ - caFile: caFile, - certFile: certFile, - keyFile: keyFile, - } -} - -func writePEMFile(t *testing.T, filename string, blockType string, bytes []byte) { - t.Helper() - //nolint:gosec - file, err := os.Create(filename) - require.NoError(t, err) - //nolint:errcheck - defer file.Close() - - err = pem.Encode(file, &pem.Block{ - Type: blockType, - Bytes: bytes, - }) - require.NoError(t, err) -} diff --git a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go index 0109d994afd..6ad652df6b5 100644 --- a/pkg/storage/unified/sql/db/dbimpl/dbimpl.go +++ b/pkg/storage/unified/sql/db/dbimpl/dbimpl.go @@ -62,62 +62,37 @@ type resourceDBProvider struct { initErr error } -func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer trace.Tracer) (p *resourceDBProvider, err error) { - // Resource API has other configs in its section besides database ones, so - // we prefix them with "db_". We use the database config from core Grafana - // as fallback, and as it uses a dedicated INI section, then keys are not - // prefixed with "db_" - getter := newConfGetter(cfg.SectionWithEnvOverrides("resource_api"), "db_") - fallbackConfig, fallbackErr := sqlstore.NewDatabaseConfig(cfg, nil) - if fallbackErr != nil { - // Ignore error here and keep going. - fallbackConfig = nil - } - - logger := log.New("entity-db") - p = &resourceDBProvider{ +func newResourceDBProvider(grafanaDB infraDB.DB, cfg *setting.Cfg, tracer trace.Tracer) (*resourceDBProvider, error) { + logger := log.New("resource-db") + p := &resourceDBProvider{ cfg: cfg, log: logger, - logQueries: getter.Bool("log_queries"), migrateFunc: migrations.MigrateResourceStore, tracer: tracer, } - - dbType := getter.String("type") - switch { - // Deprecated: First try with the config in the "resource_api" section, which is specific to Unified Storage - case dbType == dbTypePostgres: - logger.Info("Using resource_api section", "db_type", dbType) - p.registerMetrics = true - p.engine, err = getEnginePostgres(getter) - return p, err - - case dbType == dbTypeMySQL: - logger.Info("Using resource_api section", "db_type", dbType) - p.registerMetrics = true - p.engine, err = getEngineMySQL(getter) - return p, err - - case dbType != "": - return p, fmt.Errorf("invalid db type specified: %s", dbType) - - // If we have an empty Resource API db config, try with the core Grafana database config - case fallbackConfig != nil && fallbackConfig.Type != "": - logger.Info("Using database section", "db_type", fallbackConfig.Type) - p.registerMetrics = true - p.engine, err = getEngine(fallbackConfig) - return p, err - case grafanaDB != nil: - // try to use the grafana db connection (should only happen in tests) + // Try to use the grafana db connection, should only happen in tests. + if grafanaDB != nil { if newConfGetter(cfg.SectionWithEnvOverrides("database"), "").Bool(grafanaDBInstrumentQueriesKey) { return nil, errGrafanaDBInstrumentedNotSupported } p.engine = grafanaDB.GetEngine() + p.logQueries = cfg.SectionWithEnvOverrides("database").Key("log_queries").MustBool(false) return p, nil + } + + // If we don't provide a DB, lets build it from the config. + dbCfg, err := sqlstore.NewDatabaseConfig(cfg, nil) + if err != nil { + return nil, err + } + + switch { + case dbCfg.Type != "": + logger.Info("Using database section", "db_type", dbCfg.Type) + p.registerMetrics = true + p.engine, err = getEngine(dbCfg) + return p, err default: - if fallbackErr != nil { - return nil, fallbackErr - } return nil, fmt.Errorf("no database type specified") } } From e4f78c462e2ec881a8f25106b8ccd7f60b3be980 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:04:04 +0000 Subject: [PATCH 22/25] Update dependency react-virtualized-auto-sizer to v1.0.26 (#105005) * Update dependency react-virtualized-auto-sizer to v1.0.26 * fix unit tests --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ashley Harrison --- package.json | 4 +- packages/grafana-flamegraph/package.json | 4 +- .../src/FlameGraphContainer.test.tsx | 10 +++-- .../FlameGraphTopTableContainer.test.tsx | 17 ++++++-- packages/grafana-sql/package.json | 4 +- .../containers/SoloPanelPage.test.tsx | 8 +++- yarn.lock | 42 +++++++++---------- 7 files changed, 53 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 55d58e1198c..9c3c6e603d2 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "@types/react-router-dom": "5.3.3", "@types/react-table": "7.7.20", "@types/react-transition-group": "4.4.12", - "@types/react-virtualized-auto-sizer": "1.0.4", + "@types/react-virtualized-auto-sizer": "1.0.8", "@types/react-window": "1.8.8", "@types/react-window-infinite-loader": "^1", "@types/redux-mock-store": "1.5.0", @@ -402,7 +402,7 @@ "react-transition-group": "4.4.5", "react-use": "17.6.0", "react-virtual": "2.10.4", - "react-virtualized-auto-sizer": "1.0.25", + "react-virtualized-auto-sizer": "1.0.26", "react-window": "1.8.11", "react-window-infinite-loader": "1.0.10", "react-zoom-pan-pinch": "^3.3.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 93e41401ba8..892771c1082 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -51,7 +51,7 @@ "lodash": "4.17.21", "react": "18.3.1", "react-use": "17.6.0", - "react-virtualized-auto-sizer": "1.0.25", + "react-virtualized-auto-sizer": "1.0.26", "tinycolor2": "1.6.0", "tslib": "2.8.1" }, @@ -70,7 +70,7 @@ "@types/lodash": "4.17.20", "@types/node": "22.15.0", "@types/react": "18.3.18", - "@types/react-virtualized-auto-sizer": "1.0.4", + "@types/react-virtualized-auto-sizer": "1.0.8", "@types/tinycolor2": "1.4.6", "babel-jest": "29.7.0", "esbuild": "0.25.6", diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx index 94fcfa71a1c..5be3bb3c3db 100644 --- a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx +++ b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx @@ -19,9 +19,13 @@ jest.mock('react-use', () => ({ describe('FlameGraphContainer', () => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'clientWidth', { value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + left: 0, + })), + }); const FlameGraphContainerWithProps = () => { const flameGraphData = createDataFrame(data); diff --git a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx index 0396e536535..22f50cb36b8 100644 --- a/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx +++ b/packages/grafana-flamegraph/src/TopTable/FlameGraphTopTableContainer.test.tsx @@ -31,8 +31,12 @@ describe('FlameGraphTopTableContainer', () => { it('should render correctly', async () => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + })), + }); setup(); const rows = screen.getAllByRole('row'); @@ -56,8 +60,13 @@ describe('FlameGraphTopTableContainer', () => { it('should render search and sandwich buttons', async () => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + left: 0, + })), + }); const { mocks } = setup(); diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 303af8c1f7a..5807c465e12 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -29,7 +29,7 @@ "react-dom": "18.3.1", "react-select": "5.10.1", "react-use": "17.6.0", - "react-virtualized-auto-sizer": "1.0.25", + "react-virtualized-auto-sizer": "1.0.26", "rxjs": "7.8.2", "sql-formatter-plus": "^1.3.6", "tslib": "2.8.1", @@ -46,7 +46,7 @@ "@types/node": "22.15.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "@types/react-virtualized-auto-sizer": "1.0.4", + "@types/react-virtualized-auto-sizer": "1.0.8", "@types/systemjs": "6.15.3", "@types/uuid": "10.0.0", "i18next-parser": "9.3.0", diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx index 421d7456acf..786a3e165e2 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx @@ -127,8 +127,12 @@ describe('SoloPanelPage', () => { soloPanelPageScenario('Dashboard init completed ', (ctx) => { ctx.setup(() => { // Needed for AutoSizer to work in test - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 500 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 500 }); + Object.defineProperty(Element.prototype, 'getBoundingClientRect', { + value: jest.fn(() => ({ + width: 500, + height: 500, + })), + }); ctx.mount(); ctx.setDashboard(); diff --git a/yarn.lock b/yarn.lock index 192b15b61bb..450707fc536 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3231,7 +3231,7 @@ __metadata: "@types/lodash": "npm:4.17.20" "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" - "@types/react-virtualized-auto-sizer": "npm:1.0.4" + "@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/tinycolor2": "npm:1.4.6" babel-jest: "npm:29.7.0" d3: "npm:^7.8.5" @@ -3241,7 +3241,7 @@ __metadata: lodash: "npm:4.17.21" react: "npm:18.3.1" react-use: "npm:17.6.0" - react-virtualized-auto-sizer: "npm:1.0.25" + react-virtualized-auto-sizer: "npm:1.0.26" rollup: "npm:^4.22.4" rollup-plugin-esbuild: "npm:6.2.0" rollup-plugin-node-externals: "npm:^8.0.0" @@ -3634,7 +3634,7 @@ __metadata: "@types/node": "npm:22.15.0" "@types/react": "npm:18.3.18" "@types/react-dom": "npm:18.3.5" - "@types/react-virtualized-auto-sizer": "npm:1.0.4" + "@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/systemjs": "npm:6.15.3" "@types/uuid": "npm:10.0.0" i18next-parser: "npm:9.3.0" @@ -3645,7 +3645,7 @@ __metadata: react-dom: "npm:18.3.1" react-select: "npm:5.10.1" react-use: "npm:17.6.0" - react-virtualized-auto-sizer: "npm:1.0.25" + react-virtualized-auto-sizer: "npm:1.0.26" rxjs: "npm:7.8.2" sql-formatter-plus: "npm:^1.3.6" ts-jest: "npm:29.2.5" @@ -9952,12 +9952,12 @@ __metadata: languageName: node linkType: hard -"@types/react-virtualized-auto-sizer@npm:1.0.4": - version: 1.0.4 - resolution: "@types/react-virtualized-auto-sizer@npm:1.0.4" +"@types/react-virtualized-auto-sizer@npm:1.0.8": + version: 1.0.8 + resolution: "@types/react-virtualized-auto-sizer@npm:1.0.8" dependencies: - "@types/react": "npm:*" - checksum: 10/e0d41ac6cf0f48dfef45c0cd70146578f42ad83dad90911aa0dfe72b0e1ea62ca9ff56d4f43f322ec6dae2e7cb4d025aefe0e85241f7782c249aeface1050512 + react-virtualized-auto-sizer: "npm:*" + checksum: 10/faddd1ee92e87e0b621a5be4eb510b010437375b6c85cf72d764f52506c59fad93d79c159aad251677e4e2fb548ad4a7e76c59f5a701a517bcf5552a9929c4af languageName: node linkType: hard @@ -18243,7 +18243,7 @@ __metadata: "@types/react-router-dom": "npm:5.3.3" "@types/react-table": "npm:7.7.20" "@types/react-transition-group": "npm:4.4.12" - "@types/react-virtualized-auto-sizer": "npm:1.0.4" + "@types/react-virtualized-auto-sizer": "npm:1.0.8" "@types/react-window": "npm:1.8.8" "@types/react-window-infinite-loader": "npm:^1" "@types/redux-mock-store": "npm:1.5.0" @@ -18418,7 +18418,7 @@ __metadata: react-transition-group: "npm:4.4.5" react-use: "npm:17.6.0" react-virtual: "npm:2.10.4" - react-virtualized-auto-sizer: "npm:1.0.25" + react-virtualized-auto-sizer: "npm:1.0.26" react-window: "npm:1.8.11" react-window-infinite-loader: "npm:1.0.10" react-zoom-pan-pinch: "npm:^3.3.0" @@ -27341,6 +27341,16 @@ __metadata: languageName: node linkType: hard +"react-virtualized-auto-sizer@npm:*, react-virtualized-auto-sizer@npm:1.0.26, react-virtualized-auto-sizer@npm:^1.0.6": + version: 1.0.26 + resolution: "react-virtualized-auto-sizer@npm:1.0.26" + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/8f8127369fb3ae1a49987a1e7d2d136f379b71d1b031a4c169389274946eca0248e3fb9130aa8a7ebe82f4ca8aaab58c37252398f8f6220a6eafaa78b8464b6b + languageName: node + linkType: hard + "react-virtualized-auto-sizer@npm:1.0.24": version: 1.0.24 resolution: "react-virtualized-auto-sizer@npm:1.0.24" @@ -27351,16 +27361,6 @@ __metadata: languageName: node linkType: hard -"react-virtualized-auto-sizer@npm:1.0.25, react-virtualized-auto-sizer@npm:^1.0.6": - version: 1.0.25 - resolution: "react-virtualized-auto-sizer@npm:1.0.25" - peerDependencies: - react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/43678a904019f0413a3c649b5b64ea51263283120c991b285077b5075cf2ea564571f6d48b3a396b588d500d45820d1c98989cb7091e2a009e73e4faa7da9d20 - languageName: node - linkType: hard - "react-window-infinite-loader@npm:1.0.10": version: 1.0.10 resolution: "react-window-infinite-loader@npm:1.0.10" From 98995922f9e92f71d3d72fbe20b8783934a214a8 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 11 Jul 2025 15:48:27 +0100 Subject: [PATCH 23/25] Transformations: Import images with webpack (#107795) * Transformations: Import images with webpack * fix mock registry * use mockTransformationsRegistry in more places --- .../standardTransformersRegistry.ts | 10 +++++++ .../tests/mockTransformationsRegistry.ts | 2 ++ .../TransformationPickerNg.tsx | 13 ++++------ .../app/features/explore/Logs/Logs.test.tsx | 16 ++---------- .../explore/Logs/LogsMetaRow.test.tsx | 26 +++---------------- .../features/explore/Logs/LogsTable.test.tsx | 17 +++--------- .../explore/Logs/LogsTableWrap.test.tsx | 17 +++--------- .../FilterByValueTransformerEditor.tsx | 4 +++ .../HeatmapTransformerEditor.tsx | 4 +++ .../ConfigFromQueryTransformerEditor.tsx | 4 +++ .../CalculateFieldTransformerEditor.tsx | 4 +++ .../editors/ConcatenateTransformerEditor.tsx | 4 +++ .../ConvertFieldTypeTransformerEditor.tsx | 4 +++ .../editors/FilterByNameTransformerEditor.tsx | 4 +++ .../FilterByRefIdTransformerEditor.tsx | 4 +++ .../editors/FormatStringTransformerEditor.tsx | 5 ++++ .../editors/FormatTimeTransformerEditor.tsx | 4 +++ .../editors/GroupByTransformerEditor.tsx | 4 +++ .../GroupToNestedTableTransformerEditor.tsx | 4 +++ .../GroupingToMatrixTransformerEditor.tsx | 4 +++ .../editors/HistogramTransformerEditor.tsx | 4 +++ .../editors/JoinByFieldTransformerEditor.tsx | 4 +++ .../LabelsToFieldsTransformerEditor.tsx | 4 +++ .../editors/LimitTransformerEditor.tsx | 4 +++ .../editors/MergeTransformerEditor.tsx | 4 +++ .../OrganizeFieldsTransformerEditor.tsx | 4 +++ .../editors/ReduceTransformerEditor.tsx | 4 +++ .../editors/RenameByRegexTransformer.tsx | 4 +++ .../editors/SeriesToRowsTransformerEditor.tsx | 4 +++ .../editors/SortByTransformerEditor.tsx | 4 +++ .../editors/TransposeTransformerEditor.tsx | 5 ++++ .../ExtractFieldsTransformerEditor.tsx | 4 +++ .../images}/dark/calculateField.svg | 0 .../transformers/images}/dark/concatenate.svg | 0 .../images}/dark/configFromData.svg | 0 .../images}/dark/convertFieldType.svg | 0 .../images}/dark/extractFields.svg | 0 .../transformers/images}/dark/fieldLookup.svg | 0 .../images}/dark/filterByRefId.svg | 0 .../images}/dark/filterByValue.svg | 0 .../images}/dark/filterFieldsByName.svg | 0 .../images}/dark/formatString.svg | 0 .../transformers/images}/dark/formatTime.svg | 0 .../transformers/images}/dark/groupBy.svg | 0 .../images}/dark/groupByCalculation.svg | 0 .../images}/dark/groupToNestedTable.svg | 0 .../images}/dark/groupingToMatrix.svg | 0 .../transformers/images}/dark/heatmap.svg | 0 .../transformers/images}/dark/histogram.svg | 0 .../transformers/images}/dark/joinByField.svg | 0 .../images}/dark/joinByLabels.svg | 0 .../images}/dark/labelsToFields.svg | 0 .../transformers/images}/dark/limit.svg | 0 .../transformers/images}/dark/merge.svg | 0 .../transformers/images}/dark/organize.svg | 0 .../images}/dark/partitionByValues.svg | 0 .../images}/dark/prepareTimeSeries.svg | 0 .../transformers/images}/dark/reduce.svg | 0 .../transformers/images}/dark/regression.svg | 0 .../images}/dark/renameByRegex.svg | 0 .../images}/dark/rowsToFields.svg | 0 .../images}/dark/seriesToRows.svg | 0 .../transformers/images}/dark/sortBy.svg | 0 .../transformers/images}/dark/spatial.svg | 0 .../images}/dark/timeSeriesTable.svg | 0 .../transformers/images}/dark/transpose.svg | 0 .../images}/disabled/formatTime.svg | 0 .../transformers/images}/disabled/groupBy.svg | 0 .../images}/disabled/groupToNestedTable.svg | 0 .../images}/disabled/groupingToMatrix.svg | 0 .../transformers/images}/disabled/heatmap.svg | 0 .../transformers/images}/disabled/merge.svg | 0 .../images}/disabled/organize.svg | 0 .../images}/disabled/timeSeriesTable.svg | 0 .../images}/light/calculateField.svg | 0 .../images}/light/concatenate.svg | 0 .../images}/light/configFromData.svg | 0 .../images}/light/convertFieldType.svg | 0 .../images}/light/extractFields.svg | 0 .../images}/light/fieldLookup.svg | 0 .../images}/light/filterByRefId.svg | 0 .../images}/light/filterByValue.svg | 0 .../images}/light/filterFieldsByName.svg | 0 .../images}/light/formatString.svg | 0 .../transformers/images}/light/formatTime.svg | 0 .../transformers/images}/light/groupBy.svg | 0 .../images}/light/groupByCalculation.svg | 0 .../images}/light/groupToNestedTable.svg | 0 .../images}/light/groupingToMatrix.svg | 0 .../transformers/images}/light/heatmap.svg | 0 .../transformers/images}/light/histogram.svg | 0 .../images}/light/joinByField.svg | 0 .../images}/light/joinByLabels.svg | 0 .../images}/light/labelsToFields.svg | 0 .../transformers/images}/light/limit.svg | 0 .../transformers/images}/light/merge.svg | 0 .../transformers/images}/light/organize.svg | 0 .../images}/light/partitionByValues.svg | 0 .../images}/light/prepareTimeSeries.svg | 0 .../transformers/images}/light/reduce.svg | 0 .../transformers/images}/light/regression.svg | 0 .../images}/light/renameByRegex.svg | 0 .../images}/light/rowsToFields.svg | 0 .../images}/light/seriesToRows.svg | 0 .../transformers/images}/light/sortBy.svg | 0 .../transformers/images}/light/spatial.svg | 0 .../images}/light/timeSeriesTable.svg | 0 .../transformers/images}/light/transpose.svg | 0 .../JoinByLabelsTransformerEditor.tsx | 4 +++ .../FieldLookupTransformerEditor.tsx | 4 +++ .../PartitionByValuesEditor.tsx | 4 +++ .../PrepareTimeSeriesEditor.tsx | 4 +++ .../regression/regressionEditor.tsx | 4 +++ .../RowsToFieldsTransformerEditor.tsx | 4 +++ .../spatial/SpatialTransformerEditor.tsx | 4 +++ .../TimeSeriesTableTransformEditor.tsx | 4 +++ 116 files changed, 162 insertions(+), 73 deletions(-) rename public/{img/transformations => app/features/transformers/images}/dark/calculateField.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/concatenate.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/configFromData.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/convertFieldType.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/extractFields.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/fieldLookup.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/filterByRefId.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/filterByValue.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/filterFieldsByName.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/formatString.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/formatTime.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/groupBy.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/groupByCalculation.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/groupToNestedTable.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/groupingToMatrix.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/heatmap.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/histogram.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/joinByField.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/joinByLabels.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/labelsToFields.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/limit.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/merge.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/organize.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/partitionByValues.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/prepareTimeSeries.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/reduce.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/regression.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/renameByRegex.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/rowsToFields.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/seriesToRows.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/sortBy.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/spatial.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/timeSeriesTable.svg (100%) rename public/{img/transformations => app/features/transformers/images}/dark/transpose.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/formatTime.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/groupBy.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/groupToNestedTable.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/groupingToMatrix.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/heatmap.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/merge.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/organize.svg (100%) rename public/{img/transformations => app/features/transformers/images}/disabled/timeSeriesTable.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/calculateField.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/concatenate.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/configFromData.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/convertFieldType.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/extractFields.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/fieldLookup.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/filterByRefId.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/filterByValue.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/filterFieldsByName.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/formatString.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/formatTime.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/groupBy.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/groupByCalculation.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/groupToNestedTable.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/groupingToMatrix.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/heatmap.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/histogram.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/joinByField.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/joinByLabels.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/labelsToFields.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/limit.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/merge.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/organize.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/partitionByValues.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/prepareTimeSeries.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/reduce.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/regression.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/renameByRegex.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/rowsToFields.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/seriesToRows.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/sortBy.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/spatial.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/timeSeriesTable.svg (100%) rename public/{img/transformations => app/features/transformers/images}/light/transpose.svg (100%) diff --git a/packages/grafana-data/src/transformations/standardTransformersRegistry.ts b/packages/grafana-data/src/transformations/standardTransformersRegistry.ts index 54166fe33d1..01a528a4a9a 100644 --- a/packages/grafana-data/src/transformations/standardTransformersRegistry.ts +++ b/packages/grafana-data/src/transformations/standardTransformersRegistry.ts @@ -39,6 +39,16 @@ export interface TransformerRegistryItem extends RegistryItem { * Set of tags associated with the transformer for improved transformation search */ tags?: Set; + + /** + * Image representing the transformer, for dark themes + */ + imageDark: string; + + /** + * Image representing the transformer, for light themes + */ + imageLight: string; } export enum TransformerCategory { diff --git a/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts b/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts index ef1b6f78fce..e5ef149b5a3 100644 --- a/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts +++ b/packages/grafana-data/src/utils/tests/mockTransformationsRegistry.ts @@ -11,6 +11,8 @@ export const mockTransformationsRegistry = (transformers: DataTransformerInfo[]) transformation: t, description: t.description, editor: () => null, + imageDark: `build/img/${t.id}-dark.abc123.svg`, + imageLight: `build/img/${t.id}-light.abc123.svg`, }; }); }); diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx index 368cfb23e1c..dbc34734758 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationPickerNg.tsx @@ -11,8 +11,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { Badge, Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2 } from '@grafana/ui'; -import config from 'app/core/config'; +import { Badge, Card, Drawer, FilterPill, IconButton, Input, Switch, useStyles2, useTheme2 } from '@grafana/ui'; import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo'; import { getCategoriesLabels } from 'app/features/transformers/utils'; @@ -172,6 +171,7 @@ interface TransformationsGridProps { } function TransformationsGrid({ showIllustrations, transformations, onClick, data }: TransformationsGridProps) { + const theme = useTheme2(); const styles = useStyles2(getTransformationGridStyles); return ( @@ -200,6 +200,8 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data cardClasses = cx(styles.newCard, styles.cardDisabled); } + const imageUrl = theme.isDark ? transform.imageDark : transform.imageLight; + return ( {standardTransformersRegistry.getIfExists(transform.id)?.description}
{showIllustrations && ( - {transform.name} + {transform.name} )} {!isApplicable && applicabilityDescription !== null && ( @@ -308,8 +310,3 @@ function getTransformationGridStyles(theme: GrafanaTheme2) { }), }; } - -const getImagePath = (id: string, disabled: boolean) => { - const folder = config.theme2.isDark ? 'dark' : 'light'; - return `public/build/img/transformations/${folder}/${id}.svg`; -}; diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index 5135821c28f..958a8c589f8 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -10,13 +10,12 @@ import { LoadingState, LogLevel, LogRowModel, - standardTransformersRegistry, toUtc, createDataFrame, ExploreLogsPanelState, DataQuery, } from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; @@ -80,18 +79,7 @@ describe('Logs', () => { }); beforeAll(() => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); }); afterAll(() => { diff --git a/public/app/features/explore/Logs/LogsMetaRow.test.tsx b/public/app/features/explore/Logs/LogsMetaRow.test.tsx index 618be846511..598b7cebb7a 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.test.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.test.tsx @@ -3,17 +3,8 @@ import userEvent from '@testing-library/user-event'; import saveAs from 'file-saver'; import { ComponentProps } from 'react'; -import { - FieldType, - LogLevel, - LogsDedupStrategy, - LogsMetaItem, - LogsMetaKind, - standardTransformersRegistry, - store, - toDataFrame, -} from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { FieldType, LogLevel, LogsDedupStrategy, LogsMetaItem, LogsMetaKind, store, toDataFrame } from '@grafana/data'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { logRowsToReadableJson } from '../../logs/utils'; @@ -177,18 +168,7 @@ describe('LogsMetaRow', () => { it('renders a button to download CSV', async () => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); const rows = [ { diff --git a/public/app/features/explore/Logs/LogsTable.test.tsx b/public/app/features/explore/Logs/LogsTable.test.tsx index 373bd38504a..2c9cdedb95a 100644 --- a/public/app/features/explore/Logs/LogsTable.test.tsx +++ b/public/app/features/explore/Logs/LogsTable.test.tsx @@ -1,8 +1,8 @@ import { render, screen, waitFor } from '@testing-library/react'; import { ComponentProps } from 'react'; -import { DataFrame, FieldType, LogsSortOrder, standardTransformersRegistry, toUtc } from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { DataFrame, FieldType, LogsSortOrder, toUtc } from '@grafana/data'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; @@ -91,18 +91,7 @@ const setup = (partialProps?: Partial>, logs?: describe('LogsTable', () => { beforeAll(() => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); }); let originalVisualisationTypeValue = config.featureToggles.logsExploreTableVisualisation; diff --git a/public/app/features/explore/Logs/LogsTableWrap.test.tsx b/public/app/features/explore/Logs/LogsTableWrap.test.tsx index 0698f141043..b14690a32a9 100644 --- a/public/app/features/explore/Logs/LogsTableWrap.test.tsx +++ b/public/app/features/explore/Logs/LogsTableWrap.test.tsx @@ -1,8 +1,8 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { ComponentProps } from 'react'; -import { createTheme, ExploreLogsPanelState, LogsSortOrder, standardTransformersRegistry, toUtc } from '@grafana/data'; -import { organizeFieldsTransformer } from '@grafana/data/internal'; +import { createTheme, ExploreLogsPanelState, LogsSortOrder, toUtc } from '@grafana/data'; +import { mockTransformationsRegistry, organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from '../../transformers/extractFields/extractFields'; @@ -39,18 +39,7 @@ const setup = (partialProps?: Partial>) => describe('LogsTableWrap', () => { beforeAll(() => { const transformers = [extractFieldsTransformer, organizeFieldsTransformer]; - standardTransformersRegistry.setInit(() => { - return transformers.map((t) => { - return { - id: t.id, - aliasIds: t.aliasIds, - name: t.name, - transformation: t, - description: t.description, - editor: () => null, - }; - }); - }); + mockTransformationsRegistry(transformers); }); it('should render 4 table rows', async () => { diff --git a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx index fb255cbb530..366da659cf2 100644 --- a/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/FilterByValueTransformerEditor.tsx @@ -24,6 +24,8 @@ import { Trans, t } from '@grafana/i18n'; import { Button, RadioButtonGroup, InlineField, Box } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/filterByValue.svg'; +import lightImage from '../images/light/filterByValue.svg'; import { DataFrameFieldsInfo, FilterByValueFilterEditor } from './FilterByValueFilterEditor'; @@ -160,6 +162,8 @@ export const getFilterByValueTransformRegistryItem: () => TransformerRegistryIte ), categories: new Set([TransformerCategory.Filter]), help: getTransformationContent(DataTransformerID.filterByValue).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); const useFieldsInfo = (data: DataFrame[]): DataFrameFieldsInfo => { diff --git a/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx b/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx index 01d3cdd8064..e12a376629e 100644 --- a/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx +++ b/public/app/features/transformers/calculateHeatmap/HeatmapTransformerEditor.tsx @@ -10,6 +10,8 @@ import { } from '@grafana/data'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/heatmap.svg'; +import lightImage from '../images/light/heatmap.svg'; import { getDefaultOptions, getTransformerOptionPane } from '../spatial/optionsHelper'; import { addHeatmapCalculationOptions } from './editor/helper'; @@ -53,5 +55,7 @@ export const getHeatmapTransformRegistryItem: () => TransformerRegistryItem TransformerRegistryI state: PluginState.beta, categories: new Set([TransformerCategory.CalculateNewFields]), help: getTransformationContent(configFromDataTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }; }; diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx index 28d020d4e44..64d1fffba80 100644 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx @@ -27,6 +27,8 @@ import { getTemplateSrv, config as cfg } from '@grafana/runtime'; import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui'; import { getTransformationContent } from '../../docs/getTransformationContent'; +import darkImage from '../../images/dark/calculateField.svg'; +import lightImage from '../../images/light/calculateField.svg'; import { BinaryOperationOptionsEditor } from './BinaryOperationOptionsEditor'; import { CumulativeOptionsEditor } from './CumulativeOptionsEditor'; @@ -279,4 +281,6 @@ export const getCalculateFieldTransformRegistryItem: () => TransformerRegistryIt ), categories: new Set([TransformerCategory.CalculateNewFields]), help: getTransformationContent(DataTransformerID.calculateField).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx index 1318cf4de8c..e31ea83a565 100644 --- a/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConcatenateTransformerEditor.tsx @@ -13,6 +13,8 @@ import { t } from '@grafana/i18n'; import { InlineField, Input, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/concatenate.svg'; +import lightImage from '../images/light/concatenate.svg'; interface ConcatenateTransformerEditorProps extends TransformerUIProps {} @@ -112,4 +114,6 @@ export const getConcatenateTransformRegistryItem: () => TransformerRegistryItem< categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(DataTransformerID.concatenate).helperDocs, tags: new Set([t('transformers.editors.concatenate-transformer-editor.tags.combine', 'Combine')]), + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx index 0a1542f033a..d437144b415 100644 --- a/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx @@ -19,6 +19,8 @@ import { allFieldTypeIconOptions, FieldNamePicker } from '@grafana/ui/internal'; import { findField } from 'app/features/dimensions/utils'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/convertFieldType.svg'; +import lightImage from '../images/light/convertFieldType.svg'; import { getTimezoneOptions } from '../utils'; import { EnumMappingEditor } from './EnumMappingEditor'; @@ -280,4 +282,6 @@ export const getConvertFieldTypeTransformRegistryItem: () => TransformerRegistry categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.convertFieldType).helperDocs, tags: new Set([t('transformers.convert-field-type-transformer-editor.tags.format-field', 'Format field')]), + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx b/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx index 9f7f37f2e59..25cff1951b8 100644 --- a/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FilterByNameTransformerEditor.tsx @@ -17,6 +17,8 @@ import { getTemplateSrv } from '@grafana/runtime'; import { Input, FilterPill, InlineFieldRow, InlineField, InlineSwitch, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/filterFieldsByName.svg'; +import lightImage from '../images/light/filterFieldsByName.svg'; interface FilterByNameTransformerEditorProps extends TransformerUIProps {} @@ -265,4 +267,6 @@ export const getFilterFieldsByNameTransformRegistryItem: () => TransformerRegist ), categories: new Set([TransformerCategory.Filter]), help: getTransformationContent(DataTransformerID.filterFieldsByName).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx b/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx index ad679578389..d4ec966661c 100644 --- a/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FilterByRefIdTransformerEditor.tsx @@ -11,6 +11,8 @@ import { t } from '@grafana/i18n'; import { FrameMultiSelectionEditor } from 'app/plugins/panel/geomap/editor/FrameSelectionEditor'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/filterByRefId.svg'; +import lightImage from '../images/light/filterByRefId.svg'; export const FilterByRefIdTransformerEditor = (props: TransformerUIProps) => { return ( @@ -42,4 +44,6 @@ export const getFilterFramesByRefIdTransformRegistryItem: () => TransformerRegis ), categories: new Set([TransformerCategory.Filter]), help: getTransformationContent(DataTransformerID.filterByRefId).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx b/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx index e29cc29833e..9bee2e9bd29 100644 --- a/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FormatStringTransformerEditor.tsx @@ -18,6 +18,9 @@ import { Select, InlineFieldRow, InlineField } from '@grafana/ui'; import { FieldNamePicker } from '@grafana/ui/internal'; import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; +import darkImage from '../images/dark/formatString.svg'; +import lightImage from '../images/light/formatString.svg'; + function FormatStringTransfomerEditor({ input, options, @@ -131,4 +134,6 @@ export const getFormatStringTransformerRegistryItem: () => TransformerRegistryIt 'Manipulate string fields formatting.' ), categories: new Set([TransformerCategory.Reformat]), + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx index ca749cfa5c0..01baaae10cd 100644 --- a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx +++ b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx @@ -14,6 +14,8 @@ import { Trans, t } from '@grafana/i18n'; import { Select, InlineFieldRow, InlineField, Input, TextLink } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/formatTime.svg'; +import lightImage from '../images/light/formatTime.svg'; import { getTimezoneOptions } from '../utils'; export function FormatTimeTransfomerEditor({ @@ -129,4 +131,6 @@ export const getFormatTimeTransformerRegistryItem: () => TransformerRegistryItem 'Set the output format of a time field' ), help: getTransformationContent(DataTransformerID.formatTime).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx index fcde5f473de..e26aced6fcd 100644 --- a/public/app/features/transformers/editors/GroupByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupByTransformerEditor.tsx @@ -16,6 +16,8 @@ import { t } from '@grafana/i18n'; import { useTheme2, Select, StatsPicker, InlineField, Stack, Alert } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/groupBy.svg'; +import lightImage from '../images/light/groupBy.svg'; import { useAllFieldNamesFromDataFrames } from '../utils'; interface FieldProps { @@ -174,4 +176,6 @@ export const getGroupByTransformRegistryItem: () => TransformerRegistryItem TransformerRegist TransformerCategory.Reformat, ]), state: PluginState.beta, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx index 9ab69c466b2..6eb793e082a 100644 --- a/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx +++ b/public/app/features/transformers/editors/GroupingToMatrixTransformerEditor.tsx @@ -15,6 +15,8 @@ import { getTemplateSrv } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/groupingToMatrix.svg'; +import lightImage from '../images/light/groupingToMatrix.svg'; import { useAllFieldNamesFromDataFrames } from '../utils'; export const GroupingToMatrixTransformerEditor = ({ @@ -160,4 +162,6 @@ export const getGroupingToMatrixTransformRegistryItem: () => TransformerRegistry ), categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.groupingToMatrix).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx index 02e79b66e8a..c2629e2f145 100644 --- a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx +++ b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx @@ -12,6 +12,8 @@ import { t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/histogram.svg'; +import lightImage from '../images/light/histogram.svg'; import { SuggestionsInput } from '../suggestionsInput/SuggestionsInput'; import { getVariableSuggestions, numberOrVariableValidator } from '../utils'; @@ -147,4 +149,6 @@ export const getHistogramTransformRegistryItem: () => TransformerRegistryItem) { const names = useFieldDisplayNames(input); @@ -117,4 +119,6 @@ export const getJoinByFieldTransformerRegistryItem: () => TransformerRegistryIte ), categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(DataTransformerID.joinByField).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx b/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx index 939dacd5763..42944c4fb15 100644 --- a/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/LabelsToFieldsTransformerEditor.tsx @@ -13,6 +13,8 @@ import { t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, RadioButtonGroup, Select, FilterPill, Stack } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/labelsToFields.svg'; +import lightImage from '../images/light/labelsToFields.svg'; export const LabelsAsFieldsTransformerEditor = ({ input, @@ -150,4 +152,6 @@ export const getLabelsToFieldsTransformerRegistryItem: () => TransformerRegistry ), categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.labelsToFields).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/LimitTransformerEditor.tsx b/public/app/features/transformers/editors/LimitTransformerEditor.tsx index 326f02f3c82..6c2cfc7dbb9 100644 --- a/public/app/features/transformers/editors/LimitTransformerEditor.tsx +++ b/public/app/features/transformers/editors/LimitTransformerEditor.tsx @@ -12,6 +12,8 @@ import { t } from '@grafana/i18n'; import { InlineFieldRow } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/limit.svg'; +import lightImage from '../images/light/limit.svg'; import { SuggestionsInput } from '../suggestionsInput/SuggestionsInput'; import { getVariableSuggestions, numberOrVariableValidator } from '../utils'; @@ -56,4 +58,6 @@ export const getLimitTransformRegistryItem: () => TransformerRegistryItem) => { if (input.length <= 1) { @@ -36,4 +38,6 @@ export const getMergeTransformerRegistryItem: () => TransformerRegistryItem {} @@ -507,4 +509,6 @@ export const getOrganizeFieldsTransformRegistryItem: () => TransformerRegistryIt ), categories: new Set([TransformerCategory.ReorderAndRename]), help: getTransformationContent(DataTransformerID.organize).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/ReduceTransformerEditor.tsx b/public/app/features/transformers/editors/ReduceTransformerEditor.tsx index 3df242e370c..71ae8a6a08c 100644 --- a/public/app/features/transformers/editors/ReduceTransformerEditor.tsx +++ b/public/app/features/transformers/editors/ReduceTransformerEditor.tsx @@ -15,6 +15,8 @@ import { t } from '@grafana/i18n'; import { InlineField, Select, StatsPicker, InlineSwitch } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/reduce.svg'; +import lightImage from '../images/light/reduce.svg'; // TODO: Minimal implementation, needs some <3 export const ReduceTransformerEditor = ({ options, onChange }: TransformerUIProps) => { @@ -128,4 +130,6 @@ export const getReduceTransformRegistryItem: () => TransformerRegistryItem {} @@ -132,4 +134,6 @@ export const getRenameByRegexTransformRegistryItem: () => TransformerRegistryIte ), categories: new Set([TransformerCategory.ReorderAndRename]), help: getTransformationContent(DataTransformerID.renameByRegex).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx b/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx index 15ead54a4a3..154761870dc 100644 --- a/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SeriesToRowsTransformerEditor.tsx @@ -9,6 +9,8 @@ import { SeriesToRowsTransformerOptions } from '@grafana/data/internal'; import { t } from '@grafana/i18n'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/seriesToRows.svg'; +import lightImage from '../images/light/seriesToRows.svg'; export const SeriesToRowsTransformerEditor = ({ input, @@ -30,4 +32,6 @@ export const getSeriesToRowsTransformerRegistryItem: () => TransformerRegistryIt ), categories: new Set([TransformerCategory.Combine, TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.seriesToRows).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/editors/SortByTransformerEditor.tsx b/public/app/features/transformers/editors/SortByTransformerEditor.tsx index 3f76977c92f..c2382e6e27c 100644 --- a/public/app/features/transformers/editors/SortByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SortByTransformerEditor.tsx @@ -13,6 +13,8 @@ import { getTemplateSrv } from '@grafana/runtime'; import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/sortBy.svg'; +import lightImage from '../images/light/sortBy.svg'; import { useAllFieldNamesFromDataFrames } from '../utils'; export const SortByTransformerEditor = ({ input, options, onChange }: TransformerUIProps) => { @@ -72,4 +74,6 @@ export const getSortByTransformRegistryItem: () => TransformerRegistryItem) => { return ( <> @@ -58,4 +61,6 @@ export const getTransposeTransformerRegistryItem: () => TransformerRegistryItem< t('transformers.transpose-transformer-editor.tags.translate', 'Translate'), t('transformers.transpose-transformer-editor.tags.transform', 'Transform'), ]), + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx index a8b85f39c5c..f4d1773b8f2 100644 --- a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx @@ -14,6 +14,8 @@ import { InlineField, InlineFieldRow, Select, InlineSwitch, Input, Combobox, Com import { FieldNamePicker } from '@grafana/ui/internal'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/extractFields.svg'; +import lightImage from '../images/light/extractFields.svg'; import { JSONPathEditor } from './components/JSONPathEditor'; import { extractFieldsTransformer } from './extractFields'; @@ -191,4 +193,6 @@ export const getExtractFieldsTransformRegistryItem: () => TransformerRegistryIte ), categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.extractFields).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/img/transformations/dark/calculateField.svg b/public/app/features/transformers/images/dark/calculateField.svg similarity index 100% rename from public/img/transformations/dark/calculateField.svg rename to public/app/features/transformers/images/dark/calculateField.svg diff --git a/public/img/transformations/dark/concatenate.svg b/public/app/features/transformers/images/dark/concatenate.svg similarity index 100% rename from public/img/transformations/dark/concatenate.svg rename to public/app/features/transformers/images/dark/concatenate.svg diff --git a/public/img/transformations/dark/configFromData.svg b/public/app/features/transformers/images/dark/configFromData.svg similarity index 100% rename from public/img/transformations/dark/configFromData.svg rename to public/app/features/transformers/images/dark/configFromData.svg diff --git a/public/img/transformations/dark/convertFieldType.svg b/public/app/features/transformers/images/dark/convertFieldType.svg similarity index 100% rename from public/img/transformations/dark/convertFieldType.svg rename to public/app/features/transformers/images/dark/convertFieldType.svg diff --git a/public/img/transformations/dark/extractFields.svg b/public/app/features/transformers/images/dark/extractFields.svg similarity index 100% rename from public/img/transformations/dark/extractFields.svg rename to public/app/features/transformers/images/dark/extractFields.svg diff --git a/public/img/transformations/dark/fieldLookup.svg b/public/app/features/transformers/images/dark/fieldLookup.svg similarity index 100% rename from public/img/transformations/dark/fieldLookup.svg rename to public/app/features/transformers/images/dark/fieldLookup.svg diff --git a/public/img/transformations/dark/filterByRefId.svg b/public/app/features/transformers/images/dark/filterByRefId.svg similarity index 100% rename from public/img/transformations/dark/filterByRefId.svg rename to public/app/features/transformers/images/dark/filterByRefId.svg diff --git a/public/img/transformations/dark/filterByValue.svg b/public/app/features/transformers/images/dark/filterByValue.svg similarity index 100% rename from public/img/transformations/dark/filterByValue.svg rename to public/app/features/transformers/images/dark/filterByValue.svg diff --git a/public/img/transformations/dark/filterFieldsByName.svg b/public/app/features/transformers/images/dark/filterFieldsByName.svg similarity index 100% rename from public/img/transformations/dark/filterFieldsByName.svg rename to public/app/features/transformers/images/dark/filterFieldsByName.svg diff --git a/public/img/transformations/dark/formatString.svg b/public/app/features/transformers/images/dark/formatString.svg similarity index 100% rename from public/img/transformations/dark/formatString.svg rename to public/app/features/transformers/images/dark/formatString.svg diff --git a/public/img/transformations/dark/formatTime.svg b/public/app/features/transformers/images/dark/formatTime.svg similarity index 100% rename from public/img/transformations/dark/formatTime.svg rename to public/app/features/transformers/images/dark/formatTime.svg diff --git a/public/img/transformations/dark/groupBy.svg b/public/app/features/transformers/images/dark/groupBy.svg similarity index 100% rename from public/img/transformations/dark/groupBy.svg rename to public/app/features/transformers/images/dark/groupBy.svg diff --git a/public/img/transformations/dark/groupByCalculation.svg b/public/app/features/transformers/images/dark/groupByCalculation.svg similarity index 100% rename from public/img/transformations/dark/groupByCalculation.svg rename to public/app/features/transformers/images/dark/groupByCalculation.svg diff --git a/public/img/transformations/dark/groupToNestedTable.svg b/public/app/features/transformers/images/dark/groupToNestedTable.svg similarity index 100% rename from public/img/transformations/dark/groupToNestedTable.svg rename to public/app/features/transformers/images/dark/groupToNestedTable.svg diff --git a/public/img/transformations/dark/groupingToMatrix.svg b/public/app/features/transformers/images/dark/groupingToMatrix.svg similarity index 100% rename from public/img/transformations/dark/groupingToMatrix.svg rename to public/app/features/transformers/images/dark/groupingToMatrix.svg diff --git a/public/img/transformations/dark/heatmap.svg b/public/app/features/transformers/images/dark/heatmap.svg similarity index 100% rename from public/img/transformations/dark/heatmap.svg rename to public/app/features/transformers/images/dark/heatmap.svg diff --git a/public/img/transformations/dark/histogram.svg b/public/app/features/transformers/images/dark/histogram.svg similarity index 100% rename from public/img/transformations/dark/histogram.svg rename to public/app/features/transformers/images/dark/histogram.svg diff --git a/public/img/transformations/dark/joinByField.svg b/public/app/features/transformers/images/dark/joinByField.svg similarity index 100% rename from public/img/transformations/dark/joinByField.svg rename to public/app/features/transformers/images/dark/joinByField.svg diff --git a/public/img/transformations/dark/joinByLabels.svg b/public/app/features/transformers/images/dark/joinByLabels.svg similarity index 100% rename from public/img/transformations/dark/joinByLabels.svg rename to public/app/features/transformers/images/dark/joinByLabels.svg diff --git a/public/img/transformations/dark/labelsToFields.svg b/public/app/features/transformers/images/dark/labelsToFields.svg similarity index 100% rename from public/img/transformations/dark/labelsToFields.svg rename to public/app/features/transformers/images/dark/labelsToFields.svg diff --git a/public/img/transformations/dark/limit.svg b/public/app/features/transformers/images/dark/limit.svg similarity index 100% rename from public/img/transformations/dark/limit.svg rename to public/app/features/transformers/images/dark/limit.svg diff --git a/public/img/transformations/dark/merge.svg b/public/app/features/transformers/images/dark/merge.svg similarity index 100% rename from public/img/transformations/dark/merge.svg rename to public/app/features/transformers/images/dark/merge.svg diff --git a/public/img/transformations/dark/organize.svg b/public/app/features/transformers/images/dark/organize.svg similarity index 100% rename from public/img/transformations/dark/organize.svg rename to public/app/features/transformers/images/dark/organize.svg diff --git a/public/img/transformations/dark/partitionByValues.svg b/public/app/features/transformers/images/dark/partitionByValues.svg similarity index 100% rename from public/img/transformations/dark/partitionByValues.svg rename to public/app/features/transformers/images/dark/partitionByValues.svg diff --git a/public/img/transformations/dark/prepareTimeSeries.svg b/public/app/features/transformers/images/dark/prepareTimeSeries.svg similarity index 100% rename from public/img/transformations/dark/prepareTimeSeries.svg rename to public/app/features/transformers/images/dark/prepareTimeSeries.svg diff --git a/public/img/transformations/dark/reduce.svg b/public/app/features/transformers/images/dark/reduce.svg similarity index 100% rename from public/img/transformations/dark/reduce.svg rename to public/app/features/transformers/images/dark/reduce.svg diff --git a/public/img/transformations/dark/regression.svg b/public/app/features/transformers/images/dark/regression.svg similarity index 100% rename from public/img/transformations/dark/regression.svg rename to public/app/features/transformers/images/dark/regression.svg diff --git a/public/img/transformations/dark/renameByRegex.svg b/public/app/features/transformers/images/dark/renameByRegex.svg similarity index 100% rename from public/img/transformations/dark/renameByRegex.svg rename to public/app/features/transformers/images/dark/renameByRegex.svg diff --git a/public/img/transformations/dark/rowsToFields.svg b/public/app/features/transformers/images/dark/rowsToFields.svg similarity index 100% rename from public/img/transformations/dark/rowsToFields.svg rename to public/app/features/transformers/images/dark/rowsToFields.svg diff --git a/public/img/transformations/dark/seriesToRows.svg b/public/app/features/transformers/images/dark/seriesToRows.svg similarity index 100% rename from public/img/transformations/dark/seriesToRows.svg rename to public/app/features/transformers/images/dark/seriesToRows.svg diff --git a/public/img/transformations/dark/sortBy.svg b/public/app/features/transformers/images/dark/sortBy.svg similarity index 100% rename from public/img/transformations/dark/sortBy.svg rename to public/app/features/transformers/images/dark/sortBy.svg diff --git a/public/img/transformations/dark/spatial.svg b/public/app/features/transformers/images/dark/spatial.svg similarity index 100% rename from public/img/transformations/dark/spatial.svg rename to public/app/features/transformers/images/dark/spatial.svg diff --git a/public/img/transformations/dark/timeSeriesTable.svg b/public/app/features/transformers/images/dark/timeSeriesTable.svg similarity index 100% rename from public/img/transformations/dark/timeSeriesTable.svg rename to public/app/features/transformers/images/dark/timeSeriesTable.svg diff --git a/public/img/transformations/dark/transpose.svg b/public/app/features/transformers/images/dark/transpose.svg similarity index 100% rename from public/img/transformations/dark/transpose.svg rename to public/app/features/transformers/images/dark/transpose.svg diff --git a/public/img/transformations/disabled/formatTime.svg b/public/app/features/transformers/images/disabled/formatTime.svg similarity index 100% rename from public/img/transformations/disabled/formatTime.svg rename to public/app/features/transformers/images/disabled/formatTime.svg diff --git a/public/img/transformations/disabled/groupBy.svg b/public/app/features/transformers/images/disabled/groupBy.svg similarity index 100% rename from public/img/transformations/disabled/groupBy.svg rename to public/app/features/transformers/images/disabled/groupBy.svg diff --git a/public/img/transformations/disabled/groupToNestedTable.svg b/public/app/features/transformers/images/disabled/groupToNestedTable.svg similarity index 100% rename from public/img/transformations/disabled/groupToNestedTable.svg rename to public/app/features/transformers/images/disabled/groupToNestedTable.svg diff --git a/public/img/transformations/disabled/groupingToMatrix.svg b/public/app/features/transformers/images/disabled/groupingToMatrix.svg similarity index 100% rename from public/img/transformations/disabled/groupingToMatrix.svg rename to public/app/features/transformers/images/disabled/groupingToMatrix.svg diff --git a/public/img/transformations/disabled/heatmap.svg b/public/app/features/transformers/images/disabled/heatmap.svg similarity index 100% rename from public/img/transformations/disabled/heatmap.svg rename to public/app/features/transformers/images/disabled/heatmap.svg diff --git a/public/img/transformations/disabled/merge.svg b/public/app/features/transformers/images/disabled/merge.svg similarity index 100% rename from public/img/transformations/disabled/merge.svg rename to public/app/features/transformers/images/disabled/merge.svg diff --git a/public/img/transformations/disabled/organize.svg b/public/app/features/transformers/images/disabled/organize.svg similarity index 100% rename from public/img/transformations/disabled/organize.svg rename to public/app/features/transformers/images/disabled/organize.svg diff --git a/public/img/transformations/disabled/timeSeriesTable.svg b/public/app/features/transformers/images/disabled/timeSeriesTable.svg similarity index 100% rename from public/img/transformations/disabled/timeSeriesTable.svg rename to public/app/features/transformers/images/disabled/timeSeriesTable.svg diff --git a/public/img/transformations/light/calculateField.svg b/public/app/features/transformers/images/light/calculateField.svg similarity index 100% rename from public/img/transformations/light/calculateField.svg rename to public/app/features/transformers/images/light/calculateField.svg diff --git a/public/img/transformations/light/concatenate.svg b/public/app/features/transformers/images/light/concatenate.svg similarity index 100% rename from public/img/transformations/light/concatenate.svg rename to public/app/features/transformers/images/light/concatenate.svg diff --git a/public/img/transformations/light/configFromData.svg b/public/app/features/transformers/images/light/configFromData.svg similarity index 100% rename from public/img/transformations/light/configFromData.svg rename to public/app/features/transformers/images/light/configFromData.svg diff --git a/public/img/transformations/light/convertFieldType.svg b/public/app/features/transformers/images/light/convertFieldType.svg similarity index 100% rename from public/img/transformations/light/convertFieldType.svg rename to public/app/features/transformers/images/light/convertFieldType.svg diff --git a/public/img/transformations/light/extractFields.svg b/public/app/features/transformers/images/light/extractFields.svg similarity index 100% rename from public/img/transformations/light/extractFields.svg rename to public/app/features/transformers/images/light/extractFields.svg diff --git a/public/img/transformations/light/fieldLookup.svg b/public/app/features/transformers/images/light/fieldLookup.svg similarity index 100% rename from public/img/transformations/light/fieldLookup.svg rename to public/app/features/transformers/images/light/fieldLookup.svg diff --git a/public/img/transformations/light/filterByRefId.svg b/public/app/features/transformers/images/light/filterByRefId.svg similarity index 100% rename from public/img/transformations/light/filterByRefId.svg rename to public/app/features/transformers/images/light/filterByRefId.svg diff --git a/public/img/transformations/light/filterByValue.svg b/public/app/features/transformers/images/light/filterByValue.svg similarity index 100% rename from public/img/transformations/light/filterByValue.svg rename to public/app/features/transformers/images/light/filterByValue.svg diff --git a/public/img/transformations/light/filterFieldsByName.svg b/public/app/features/transformers/images/light/filterFieldsByName.svg similarity index 100% rename from public/img/transformations/light/filterFieldsByName.svg rename to public/app/features/transformers/images/light/filterFieldsByName.svg diff --git a/public/img/transformations/light/formatString.svg b/public/app/features/transformers/images/light/formatString.svg similarity index 100% rename from public/img/transformations/light/formatString.svg rename to public/app/features/transformers/images/light/formatString.svg diff --git a/public/img/transformations/light/formatTime.svg b/public/app/features/transformers/images/light/formatTime.svg similarity index 100% rename from public/img/transformations/light/formatTime.svg rename to public/app/features/transformers/images/light/formatTime.svg diff --git a/public/img/transformations/light/groupBy.svg b/public/app/features/transformers/images/light/groupBy.svg similarity index 100% rename from public/img/transformations/light/groupBy.svg rename to public/app/features/transformers/images/light/groupBy.svg diff --git a/public/img/transformations/light/groupByCalculation.svg b/public/app/features/transformers/images/light/groupByCalculation.svg similarity index 100% rename from public/img/transformations/light/groupByCalculation.svg rename to public/app/features/transformers/images/light/groupByCalculation.svg diff --git a/public/img/transformations/light/groupToNestedTable.svg b/public/app/features/transformers/images/light/groupToNestedTable.svg similarity index 100% rename from public/img/transformations/light/groupToNestedTable.svg rename to public/app/features/transformers/images/light/groupToNestedTable.svg diff --git a/public/img/transformations/light/groupingToMatrix.svg b/public/app/features/transformers/images/light/groupingToMatrix.svg similarity index 100% rename from public/img/transformations/light/groupingToMatrix.svg rename to public/app/features/transformers/images/light/groupingToMatrix.svg diff --git a/public/img/transformations/light/heatmap.svg b/public/app/features/transformers/images/light/heatmap.svg similarity index 100% rename from public/img/transformations/light/heatmap.svg rename to public/app/features/transformers/images/light/heatmap.svg diff --git a/public/img/transformations/light/histogram.svg b/public/app/features/transformers/images/light/histogram.svg similarity index 100% rename from public/img/transformations/light/histogram.svg rename to public/app/features/transformers/images/light/histogram.svg diff --git a/public/img/transformations/light/joinByField.svg b/public/app/features/transformers/images/light/joinByField.svg similarity index 100% rename from public/img/transformations/light/joinByField.svg rename to public/app/features/transformers/images/light/joinByField.svg diff --git a/public/img/transformations/light/joinByLabels.svg b/public/app/features/transformers/images/light/joinByLabels.svg similarity index 100% rename from public/img/transformations/light/joinByLabels.svg rename to public/app/features/transformers/images/light/joinByLabels.svg diff --git a/public/img/transformations/light/labelsToFields.svg b/public/app/features/transformers/images/light/labelsToFields.svg similarity index 100% rename from public/img/transformations/light/labelsToFields.svg rename to public/app/features/transformers/images/light/labelsToFields.svg diff --git a/public/img/transformations/light/limit.svg b/public/app/features/transformers/images/light/limit.svg similarity index 100% rename from public/img/transformations/light/limit.svg rename to public/app/features/transformers/images/light/limit.svg diff --git a/public/img/transformations/light/merge.svg b/public/app/features/transformers/images/light/merge.svg similarity index 100% rename from public/img/transformations/light/merge.svg rename to public/app/features/transformers/images/light/merge.svg diff --git a/public/img/transformations/light/organize.svg b/public/app/features/transformers/images/light/organize.svg similarity index 100% rename from public/img/transformations/light/organize.svg rename to public/app/features/transformers/images/light/organize.svg diff --git a/public/img/transformations/light/partitionByValues.svg b/public/app/features/transformers/images/light/partitionByValues.svg similarity index 100% rename from public/img/transformations/light/partitionByValues.svg rename to public/app/features/transformers/images/light/partitionByValues.svg diff --git a/public/img/transformations/light/prepareTimeSeries.svg b/public/app/features/transformers/images/light/prepareTimeSeries.svg similarity index 100% rename from public/img/transformations/light/prepareTimeSeries.svg rename to public/app/features/transformers/images/light/prepareTimeSeries.svg diff --git a/public/img/transformations/light/reduce.svg b/public/app/features/transformers/images/light/reduce.svg similarity index 100% rename from public/img/transformations/light/reduce.svg rename to public/app/features/transformers/images/light/reduce.svg diff --git a/public/img/transformations/light/regression.svg b/public/app/features/transformers/images/light/regression.svg similarity index 100% rename from public/img/transformations/light/regression.svg rename to public/app/features/transformers/images/light/regression.svg diff --git a/public/img/transformations/light/renameByRegex.svg b/public/app/features/transformers/images/light/renameByRegex.svg similarity index 100% rename from public/img/transformations/light/renameByRegex.svg rename to public/app/features/transformers/images/light/renameByRegex.svg diff --git a/public/img/transformations/light/rowsToFields.svg b/public/app/features/transformers/images/light/rowsToFields.svg similarity index 100% rename from public/img/transformations/light/rowsToFields.svg rename to public/app/features/transformers/images/light/rowsToFields.svg diff --git a/public/img/transformations/light/seriesToRows.svg b/public/app/features/transformers/images/light/seriesToRows.svg similarity index 100% rename from public/img/transformations/light/seriesToRows.svg rename to public/app/features/transformers/images/light/seriesToRows.svg diff --git a/public/img/transformations/light/sortBy.svg b/public/app/features/transformers/images/light/sortBy.svg similarity index 100% rename from public/img/transformations/light/sortBy.svg rename to public/app/features/transformers/images/light/sortBy.svg diff --git a/public/img/transformations/light/spatial.svg b/public/app/features/transformers/images/light/spatial.svg similarity index 100% rename from public/img/transformations/light/spatial.svg rename to public/app/features/transformers/images/light/spatial.svg diff --git a/public/img/transformations/light/timeSeriesTable.svg b/public/app/features/transformers/images/light/timeSeriesTable.svg similarity index 100% rename from public/img/transformations/light/timeSeriesTable.svg rename to public/app/features/transformers/images/light/timeSeriesTable.svg diff --git a/public/img/transformations/light/transpose.svg b/public/app/features/transformers/images/light/transpose.svg similarity index 100% rename from public/img/transformations/light/transpose.svg rename to public/app/features/transformers/images/light/transpose.svg diff --git a/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx b/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx index a32f09bf478..d7ea6b75503 100644 --- a/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx +++ b/public/app/features/transformers/joinByLabels/JoinByLabelsTransformerEditor.tsx @@ -12,6 +12,8 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, HorizontalGroup, InlineField, InlineFieldRow, Select, ValuePicker } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/joinByLabels.svg'; +import lightImage from '../images/light/joinByLabels.svg'; import { getDistinctLabels } from '../utils'; import { getJoinByLabelsTransformer, JoinByLabelsTransformOptions } from './joinByLabels'; @@ -189,5 +191,7 @@ export const getJoinByLabelsTransformRegistryItem: () => TransformerRegistryItem state: PluginState.beta, categories: new Set([TransformerCategory.Combine]), help: getTransformationContent(joinByLabelsTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }; }; diff --git a/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx b/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx index b21e8a42566..d4829275781 100644 --- a/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx +++ b/public/app/features/transformers/lookupGazetteer/FieldLookupTransformerEditor.tsx @@ -16,6 +16,8 @@ import { FieldNamePicker } from '@grafana/ui/internal'; import { GazetteerPathEditor, GazetteerPathEditorConfigSettings } from 'app/features/geo/editor/GazetteerPathEditor'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/fieldLookup.svg'; +import lightImage from '../images/light/fieldLookup.svg'; import { FieldLookupOptions, fieldLookupTransformer } from './fieldLookup'; @@ -102,4 +104,6 @@ export const getFieldLookupTransformRegistryItem: () => TransformerRegistryItem< state: PluginState.alpha, categories: new Set([TransformerCategory.PerformSpatialOperations]), help: getTransformationContent(DataTransformerID.fieldLookup).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }); diff --git a/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx b/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx index b974a7d4025..e217384f051 100644 --- a/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx +++ b/public/app/features/transformers/partitionByValues/PartitionByValuesEditor.tsx @@ -21,6 +21,8 @@ import { import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/internal'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/partitionByValues.svg'; +import lightImage from '../images/light/partitionByValues.svg'; import { getPartitionByValuesTransformer, PartitionByValuesTransformerOptions } from './partitionByValues'; @@ -190,5 +192,7 @@ export const getPartitionByValuesTransformRegistryItem: () => TransformerRegistr state: PluginState.alpha, categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(DataTransformerID.partitionByValues).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }; }; diff --git a/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx b/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx index d720744a070..c9e6b09f299 100644 --- a/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx +++ b/public/app/features/transformers/prepareTimeSeries/PrepareTimeSeriesEditor.tsx @@ -13,6 +13,8 @@ import { Trans, t } from '@grafana/i18n'; import { InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/prepareTimeSeries.svg'; +import lightImage from '../images/light/prepareTimeSeries.svg'; import { PrepareTimeSeriesOptions, timeSeriesFormat, getPrepareTimeSeriesTransformer } from './prepareTimeSeries'; @@ -179,5 +181,7 @@ export const getPrepareTimeseriesTransformerRegistryItem: () => TransformerRegis description: prepareTimeSeriesTransformer.description, categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(prepareTimeSeriesTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }; }; diff --git a/public/app/features/transformers/regression/regressionEditor.tsx b/public/app/features/transformers/regression/regressionEditor.tsx index 246ce6e6fc9..abf9b862b19 100644 --- a/public/app/features/transformers/regression/regressionEditor.tsx +++ b/public/app/features/transformers/regression/regressionEditor.tsx @@ -15,6 +15,8 @@ import { FieldNamePicker } from '@grafana/ui/internal'; import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/regression.svg'; +import lightImage from '../images/light/regression.svg'; import { DEFAULTS, DEGREES, ModelType, getRegressionTransformer, RegressionTransformerOptions } from './regression'; @@ -167,5 +169,7 @@ export const getRegressionTransformerRegistryItem: () => TransformerRegistryItem description: regressionTransformer.description, categories: new Set([TransformerCategory.CalculateNewFields]), help: getTransformationContent(DataTransformerID.regression).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }; }; diff --git a/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx b/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx index 0eb8532f840..ed111c06b03 100644 --- a/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/rowsToFields/RowsToFieldsTransformerEditor.tsx @@ -2,6 +2,8 @@ import { PluginState, TransformerRegistryItem, TransformerUIProps, TransformerCa import { getTransformationContent } from '../docs/getTransformationContent'; import { FieldToConfigMappingEditor } from '../fieldToConfigMapping/FieldToConfigMappingEditor'; +import darkImage from '../images/dark/rowsToFields.svg'; +import lightImage from '../images/light/rowsToFields.svg'; import { getRowsToFieldsTransformer, RowToFieldsTransformOptions } from './rowsToFields'; @@ -35,5 +37,7 @@ export const getRowsToFieldsTransformRegistryItem: () => TransformerRegistryItem state: PluginState.beta, categories: new Set([TransformerCategory.Reformat]), help: getTransformationContent(rowsToFieldsTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }; }; diff --git a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx index dbc94ad0f0e..8d63ed60746 100644 --- a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx +++ b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx @@ -17,6 +17,8 @@ import { useTheme2 } from '@grafana/ui'; import { addLocationFields } from 'app/features/geo/editor/locationEditor'; import { getTransformationContent } from '../docs/getTransformationContent'; +import darkImage from '../images/dark/spatial.svg'; +import lightImage from '../images/light/spatial.svg'; import { SpatialCalculation, SpatialOperation, SpatialAction, SpatialTransformOptions } from './models.gen'; import { getDefaultOptions, getTransformerOptionPane } from './optionsHelper'; @@ -184,5 +186,7 @@ export const getSpatialTransformRegistryItem: () => TransformerRegistryItem TransformerRegistryI description: timeSeriesTableTransformer.description, state: PluginState.beta, help: getTransformationContent(timeSeriesTableTransformer.id).helperDocs, + imageDark: darkImage, + imageLight: lightImage, }; }; From 40da94cf74ecfda9f621cd0c4d08d2de94657d79 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:19:40 -0500 Subject: [PATCH 24/25] Annotations: Allow `target="_blank"` (#106301) Co-authored-by: Kristian Bremberg --- .../grafana-data/src/text/sanitize.test.ts | 20 +++++++++++++++++++ packages/grafana-data/src/text/sanitize.ts | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/grafana-data/src/text/sanitize.test.ts b/packages/grafana-data/src/text/sanitize.test.ts index c3ce3d3eb90..a0c69838976 100644 --- a/packages/grafana-data/src/text/sanitize.test.ts +++ b/packages/grafana-data/src/text/sanitize.test.ts @@ -55,6 +55,26 @@ describe('sanitize', () => { const str = sanitize(html); expect(str).toBe(''); }); + + describe('should sanitize anchors with target="_blank"', () => { + it('should add rel="noopener noreferrer" to target="_blank" links', () => { + const html = 'Link'; + const str = sanitize(html); + expect(str).toBe('Link'); + }); + + it('should preserve existing rel attributes and add noopener noreferrer, if not already added', () => { + const html = 'Link'; + const str = sanitize(html); + expect(str).toBe('Link'); + }); + + it('should not modify links without target="_blank"', () => { + const html = 'Link'; + const str = sanitize(html); + expect(str).toBe('Link'); + }); + }); }); describe('validatePath', () => { diff --git a/packages/grafana-data/src/text/sanitize.ts b/packages/grafana-data/src/text/sanitize.ts index 0c3dadd5ebf..4d22046cb31 100644 --- a/packages/grafana-data/src/text/sanitize.ts +++ b/packages/grafana-data/src/text/sanitize.ts @@ -56,13 +56,22 @@ const sanitizeTextPanelWhitelist = new xss.FilterXSS({ */ export function sanitize(unsanitizedString: string): string { try { + DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (node.tagName === 'A' && node.getAttribute('target') === '_blank') { + node.setAttribute('rel', 'noopener noreferrer'); + } + }); + return DOMPurify.sanitize(unsanitizedString, { USE_PROFILES: { html: true }, FORBID_TAGS: ['form', 'input'], + ADD_ATTR: ['target'], }); } catch (error) { console.error('String could not be sanitized', unsanitizedString); return escapeHtml(unsanitizedString); + } finally { + DOMPurify.removeHook('afterSanitizeAttributes'); } } From 519e0449e78dba3c826262e03d12f95aed62e42b Mon Sep 17 00:00:00 2001 From: antonio <45235678+tonypowa@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:45:31 +0200 Subject: [PATCH 25/25] add video to get started with grafana alerting 1 (#107952) --- docs/sources/tutorials/alerting-get-started/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index 23f3894a84e..542190188f1 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -24,6 +24,8 @@ killercoda: In this tutorial, we walk you through the process of setting up your first alert in just a few minutes. You'll witness your alert in action with real-time data, as well as sending alert notifications. +{{< youtube id="YVhKNbQNNss" >}} + In this tutorial you will: - Create a contact point.