Merge branch 'main' into drclau/unistor/replace-authenticators-3

This commit is contained in:
Claudiu Dragalina-Paraipan 2024-08-28 15:46:04 +03:00
commit b766bfb24f
297 changed files with 6654 additions and 2552 deletions

View File

@ -199,7 +199,7 @@ const getChangeLogItems = async (name, owner, sinceDate, to) => {
hasLabel({ labels }, 'area/grafana/ui') ||
hasLabel({ labels }, 'area/grafana/toolkit') ||
hasLabel({ labels }, 'area/grafana/runtime');
const author = item.commits.nodes[0].commit.author.user.login;
const author = item.commits.nodes[0].commit.author.user?.login;
return {
repo: name,
number,
@ -285,7 +285,7 @@ ${items
`- ${item.title.replace(/^([^:]*:)/gm, '**$1**')} ${
item.repo === 'grafana-enterprise'
? '(Enterprise)'
: `${pullRequestLink(item.number)}, ${userLink(item.author)}`
: `${pullRequestLink(item.number)}${item.author ? ', ' + userLink(item.author) : ''}`
}`
)
.join('\n')}

View File

@ -2,6 +2,10 @@ name: Generate changelog
on:
workflow_call:
inputs:
previous_version:
type: string
required: false
description: 'The release version (semver, git tag, branch or commit) to use for comparison'
version:
type: string
required: true
@ -26,6 +30,10 @@ on:
workflow_dispatch:
inputs:
previous_version:
type: string
required: false
description: 'The release version (semver, git tag, branch or commit) to use for comparison'
version:
type: string
required: true
@ -79,6 +87,7 @@ jobs:
id: changelog
uses: ./.github/workflows/actions/changelog
with:
previous: ${{ inputs.previous_version }}
github_token: ${{ steps.generate_token.outputs.token }}
target: v${{ inputs.version }}
output_file: changelog_items.md

View File

@ -8,6 +8,10 @@ name: Complete a Grafana release
on:
workflow_dispatch:
inputs:
previous_version:
type: string
required: false
description: 'The release version (semver, git tag, branch or commit) to use for comparison'
version:
required: true
type: string
@ -38,6 +42,7 @@ jobs:
name: Create PR to main to update the changelog
uses: ./.github/workflows/changelog.yml
with:
previous_version: ${{inputs.previous_version}}
version: ${{ inputs.version }}
latest: ${{ inputs.latest }}
dry_run: ${{ inputs.dry_run }}

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: 'https://mskelton.dev/yarn-outdated/v2'
yarnPath: .yarn/releases/yarn-4.4.0.cjs
yarnPath: .yarn/releases/yarn-4.4.1.cjs
# Uncomment the following lines if you want to use Verdaccio local npm registry. Read more at packages/README.md
#npmScopes:
# grafana:

View File

@ -1,3 +1,251 @@
<!-- 11.2.0 START -->
# 11.2.0 (2024-08-27)
### Features and enhancements
- **@grafana/data:** Introduce new getTagKeys/getTagValues response interface [#88369](https://github.com/grafana/grafana/pull/88369), [@kaydelaney](https://github.com/kaydelaney)
- **AWS:** Update deprecated aws-sdk functions from env variable versions [#89643](https://github.com/grafana/grafana/pull/89643), [@iwysiu](https://github.com/iwysiu)
- **Alerting:** Add ha_reconnect_timeout configuration option [#88823](https://github.com/grafana/grafana/pull/88823), [@JacobValdemar](https://github.com/JacobValdemar)
- **Alerting:** Add setting for maximum allowed rule evaluation results [#89468](https://github.com/grafana/grafana/pull/89468), [@alexander-akhmetov](https://github.com/alexander-akhmetov)
- **Alerting:** Add warning in telegram contact point [#89397](https://github.com/grafana/grafana/pull/89397), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Central alert history part4 [#90088](https://github.com/grafana/grafana/pull/90088), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Don't crash the page when trying to filter rules by regex [#89466](https://github.com/grafana/grafana/pull/89466), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Enable remote primary mode using feature toggles [#88976](https://github.com/grafana/grafana/pull/88976), [@santihernandezc](https://github.com/santihernandezc)
- **Alerting:** Hide edit/view rule buttons according to deleting/creating state [#90375](https://github.com/grafana/grafana/pull/90375), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Implement UI for grafana-managed recording rules [#90360](https://github.com/grafana/grafana/pull/90360), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Improve performance of /api/prometheus for large numbers of alerts. [#89268](https://github.com/grafana/grafana/pull/89268), [@stevesg](https://github.com/stevesg)
- **Alerting:** Include a list of ref_Id and aggregated datasource UIDs to alerts when state reason is NoData [#88819](https://github.com/grafana/grafana/pull/88819), [@wasim-nihal](https://github.com/wasim-nihal)
- **Alerting:** Instrument outbound requests for Loki Historian and Remote Alertmanager with tracing [#89185](https://github.com/grafana/grafana/pull/89185), [@alexweav](https://github.com/alexweav)
- **Alerting:** Limit instances on alert detail view unless in instances tab [#89368](https://github.com/grafana/grafana/pull/89368), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Make alert group editing safer [#88627](https://github.com/grafana/grafana/pull/88627), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Make whitespace more visible on labels [#90223](https://github.com/grafana/grafana/pull/90223), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Remove option to return settings from api/v1/receivers and restrict provisioning action access [#90861](https://github.com/grafana/grafana/pull/90861), [@JacobsonMT](https://github.com/JacobsonMT)
- **Alerting:** Resend resolved notifications for ResolvedRetention duration [#88938](https://github.com/grafana/grafana/pull/88938), [@JacobsonMT](https://github.com/JacobsonMT)
- **Alerting:** Show Insights page only on cloud (when required ds's are available) [#89679](https://github.com/grafana/grafana/pull/89679), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Show repeat interval in timing options meta [#89414](https://github.com/grafana/grafana/pull/89414), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Support median in reduce expressions [#91119](https://github.com/grafana/grafana/pull/91119), [@alexander-akhmetov](https://github.com/alexander-akhmetov)
- **Alerting:** Track central ash interactions [#90330](https://github.com/grafana/grafana/pull/90330), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Update alerting state history API to authorize access using RBAC [#89579](https://github.com/grafana/grafana/pull/89579), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Update warning message for Telegram parse_mode and default to empty value [#89630](https://github.com/grafana/grafana/pull/89630), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Use Runbook URL label everywhere and add validation in the alert rule… [#90523](https://github.com/grafana/grafana/pull/90523), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Use cloud notifier types for metadata on Cloud AMs [#91054](https://github.com/grafana/grafana/pull/91054), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Use stable identifier of a group when export to HCL [#90196](https://github.com/grafana/grafana/pull/90196), [@KyriosGN0](https://github.com/KyriosGN0)
- **Alerting:** Use stable identifier of a group,contact point,mute timing when export to HCL [#90917](https://github.com/grafana/grafana/pull/90917), [@KyriosGN0](https://github.com/KyriosGN0)
- **Alertmanager:** Support limits for silences [#90826](https://github.com/grafana/grafana/pull/90826), [@santihernandezc](https://github.com/santihernandezc)
- **Angular deprecation:** Disable dynamic angular inspector if CheckForPluginUpdates is false [#91194](https://github.com/grafana/grafana/pull/91194), [@xnyo](https://github.com/xnyo)
- **App events:** Add "info" variant [#89903](https://github.com/grafana/grafana/pull/89903), [@Clarity-89](https://github.com/Clarity-89)
- **Auth:** Add org to role mappings support to AzureAD/Entra integration [#88861](https://github.com/grafana/grafana/pull/88861), [@mgyongyosi](https://github.com/mgyongyosi)
- **Auth:** Add organization mapping configuration to the UI [#90003](https://github.com/grafana/grafana/pull/90003), [@mgyongyosi](https://github.com/mgyongyosi)
- **Auth:** Add support for escaping colon characters in org_mapping [#89951](https://github.com/grafana/grafana/pull/89951), [@mgyongyosi](https://github.com/mgyongyosi)
- **Azure:** Add new Azure infrastructure dashboards [#88869](https://github.com/grafana/grafana/pull/88869), [@yves-chan](https://github.com/yves-chan)
- **BrowseDashboards:** Update results when starred param changes [#89944](https://github.com/grafana/grafana/pull/89944), [@Clarity-89](https://github.com/Clarity-89)
- **Caching:** Handle memcached reconnects [#91498](https://github.com/grafana/grafana/pull/91498), [@mmandrus](https://github.com/mmandrus)
- **Calendar:** Add labels for next/previous month [#89019](https://github.com/grafana/grafana/pull/89019), [@ashharrison90](https://github.com/ashharrison90)
- **Canvas:** Element level data links [#89079](https://github.com/grafana/grafana/pull/89079), [@adela-almasan](https://github.com/adela-almasan)
- **Canvas:** Improved tooltip [#90162](https://github.com/grafana/grafana/pull/90162), [@adela-almasan](https://github.com/adela-almasan)
- **Canvas:** Support template variables in base URL of actions [#91227](https://github.com/grafana/grafana/pull/91227), [@nmarrs](https://github.com/nmarrs)
- **Chore:** Add missing build elements to Dockerfile [#89714](https://github.com/grafana/grafana/pull/89714), [@azilly-de](https://github.com/azilly-de)
- **Chore:** Add unit test for cloudmigration package [#88868](https://github.com/grafana/grafana/pull/88868), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **Chore:** Commit results of bingo get [#90256](https://github.com/grafana/grafana/pull/90256), [@mmandrus](https://github.com/mmandrus)
- **CloudMigrations:** Change onPremToCloudMigrations feature toggle to public preview [#90757](https://github.com/grafana/grafana/pull/90757), [@mmandrus](https://github.com/mmandrus)
- **CloudWatch:** Add errorsource for QueryData [#91085](https://github.com/grafana/grafana/pull/91085), [@iwysiu](https://github.com/iwysiu)
- **CloudWatch:** Update grafana-aws-sdk for updated metrics [#91364](https://github.com/grafana/grafana/pull/91364), [@iwysiu](https://github.com/iwysiu)
- **Cloudwatch:** Clear cached PDC transport when PDC is disabled [#91357](https://github.com/grafana/grafana/pull/91357), [@njvrzm](https://github.com/njvrzm)
- **Cloudwatch:** Metrics Query Builder should clear old query [#88950](https://github.com/grafana/grafana/pull/88950), [@iwysiu](https://github.com/iwysiu)
- **Cloudwatch:** Remove awsDatasourcesNewFormStyling feature toggle [#90128](https://github.com/grafana/grafana/pull/90128), [@idastambuk](https://github.com/idastambuk)
- **Cloudwatch:** Rename Metric Query to Metric Insights [#89955](https://github.com/grafana/grafana/pull/89955), [@idastambuk](https://github.com/idastambuk)
- **Cloudwatch:** Round up endTime in GetMetricData to next minute [#89341](https://github.com/grafana/grafana/pull/89341), [@idastambuk](https://github.com/idastambuk)
- **Dashboard:** Use preferred timezone on create [#89833](https://github.com/grafana/grafana/pull/89833), [@Clarity-89](https://github.com/Clarity-89)
- **Datalinks:** UX improvements [#91352](https://github.com/grafana/grafana/pull/91352), [@adela-almasan](https://github.com/adela-almasan)
- **DateTimePicker:** Add "timeZone" prop [#90031](https://github.com/grafana/grafana/pull/90031), [@Clarity-89](https://github.com/Clarity-89)
- **Dynatrace:** Add to list of DS with custom label logic [#90258](https://github.com/grafana/grafana/pull/90258), [@fabrizio-grafana](https://github.com/fabrizio-grafana)
- **Elasticsearch:** Decouple backend from infra/http [#90408](https://github.com/grafana/grafana/pull/90408), [@njvrzm](https://github.com/njvrzm)
- **Elasticsearch:** Decouple backend from infra/log [#90527](https://github.com/grafana/grafana/pull/90527), [@njvrzm](https://github.com/njvrzm)
- **Elasticsearch:** Decouple backend from infra/tracing [#90528](https://github.com/grafana/grafana/pull/90528), [@njvrzm](https://github.com/njvrzm)
- **Explore:** Add setting for default time offset [#90401](https://github.com/grafana/grafana/pull/90401), [@gelicia](https://github.com/gelicia)
- **Feat:** Extending report interaction with static context that can be appended to all interaction events [#88927](https://github.com/grafana/grafana/pull/88927), [@tolzhabayev](https://github.com/tolzhabayev)
- **Feature management:** Add openSearchBackendFlowEnabled feature toggle [#89208](https://github.com/grafana/grafana/pull/89208), [@idastambuk](https://github.com/idastambuk)
- **Features:** Add cloudwatchMetricInsightsCrossAccount feature toggle [#89848](https://github.com/grafana/grafana/pull/89848), [@idastambuk](https://github.com/idastambuk)
- **Features:** Release Cloudwatch Metric Insights cross-account querying to public preview [#91066](https://github.com/grafana/grafana/pull/91066), [@idastambuk](https://github.com/idastambuk)
- **FlameGraph:** Remove flameGraphItemCollapsing feature toggle [#90190](https://github.com/grafana/grafana/pull/90190), [@joey-grafana](https://github.com/joey-grafana)
- **GCP:** Update GKE monitoring dashboard [#90091](https://github.com/grafana/grafana/pull/90091), [@aangelisc](https://github.com/aangelisc)
- **GOps:** Add Grafana SLO steps to IRM configuration tracker [#88098](https://github.com/grafana/grafana/pull/88098), [@obetomuniz](https://github.com/obetomuniz)
- **Grafana:** Enables use of encrypted certificates with password for https [#91418](https://github.com/grafana/grafana/pull/91418), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **IDToken:** Add current user's DisplayName to the ID token [#90992](https://github.com/grafana/grafana/pull/90992), [@colin-stuart](https://github.com/colin-stuart)
- **IDToken:** Add current user's Username and UID to the ID token [#90240](https://github.com/grafana/grafana/pull/90240), [@mgyongyosi](https://github.com/mgyongyosi)
- **Keybinds:** Allow move time range shortcuts (t left / t right) to be chained [#88904](https://github.com/grafana/grafana/pull/88904), [@joshhunt](https://github.com/joshhunt)
- **LibraryPanels:** Use new folder picker when creating a library panel [#89228](https://github.com/grafana/grafana/pull/89228), [@joshhunt](https://github.com/joshhunt)
- **Log:** Added panel support for filtering callbacks [#88980](https://github.com/grafana/grafana/pull/88980), [@matyax](https://github.com/matyax)
- **Logs:** Add log line to content outline when clicking on datalinks [#90207](https://github.com/grafana/grafana/pull/90207), [@gtk-grafana](https://github.com/gtk-grafana)
- **Loki:** Add option to issue forward queries [#91181](https://github.com/grafana/grafana/pull/91181), [@svennergr](https://github.com/svennergr)
- **Loki:** Added support for negative numbers in LogQL [#88719](https://github.com/grafana/grafana/pull/88719), [@matyax](https://github.com/matyax)
- **Loki:** Also replace `step` with vars [#91031](https://github.com/grafana/grafana/pull/91031), [@svennergr](https://github.com/svennergr)
- **Loki:** Remove `instant` query type from Log queries [#90137](https://github.com/grafana/grafana/pull/90137), [@svennergr](https://github.com/svennergr)
- **Loki:** Respect pre-selected filters in adhoc filter queries [#89022](https://github.com/grafana/grafana/pull/89022), [@ivanahuckova](https://github.com/ivanahuckova)
- **MSSQL:** Password auth for Azure AD [#89746](https://github.com/grafana/grafana/pull/89746), [@bossinc](https://github.com/bossinc)
- **Metrics:** Add ability to disable classic histogram for HTTP metric [#88315](https://github.com/grafana/grafana/pull/88315), [@hairyhenderson](https://github.com/hairyhenderson)
- **Nav:** Add items to saved [#89908](https://github.com/grafana/grafana/pull/89908), [@Clarity-89](https://github.com/Clarity-89)
- **OpenAPI:** Document the `/api/health` endpoint [#88203](https://github.com/grafana/grafana/pull/88203), [@julienduchesne](https://github.com/julienduchesne)
- **PanelChrome:** Use labelledby for accessible title [#88781](https://github.com/grafana/grafana/pull/88781), [@tskarhed](https://github.com/tskarhed)
- **Plugins:** Add filters by update available [#91526](https://github.com/grafana/grafana/pull/91526), [@oshirohugo](https://github.com/oshirohugo)
- **Plugins:** Add logs to for plugin management actions [#90587](https://github.com/grafana/grafana/pull/90587), [@oshirohugo](https://github.com/oshirohugo)
- **Plugins:** Disable install controls for provisioned plugin in cloud [#90479](https://github.com/grafana/grafana/pull/90479), [@oshirohugo](https://github.com/oshirohugo)
- **Plugins:** Expose functions to plugins for checking RBAC permissions [#89047](https://github.com/grafana/grafana/pull/89047), [@jackw](https://github.com/jackw)
- **Plugins:** Improve levitate / breaking changes report in grafana/grafana [#89822](https://github.com/grafana/grafana/pull/89822), [@oshirohugo](https://github.com/oshirohugo)
- **Plugins:** Support > 1 levels of plugin dependencies [#90174](https://github.com/grafana/grafana/pull/90174), [@wbrowne](https://github.com/wbrowne)
- **Plugins:** Update CLI check if plugin is already installed [#91213](https://github.com/grafana/grafana/pull/91213), [@wbrowne](https://github.com/wbrowne)
- **Prometheus:** Deprecation message for SigV4 in core Prom [#90250](https://github.com/grafana/grafana/pull/90250), [@bohandley](https://github.com/bohandley)
- **Prometheus:** Reintroduce Azure audience override feature flag [#90339](https://github.com/grafana/grafana/pull/90339), [@aangelisc](https://github.com/aangelisc)
- **RBAC:** Allow plugins to use scoped actions [#90946](https://github.com/grafana/grafana/pull/90946), [@gamab](https://github.com/gamab)
- **RBAC:** Default to plugins.app:access for plugin includes [#90969](https://github.com/grafana/grafana/pull/90969), [@gamab](https://github.com/gamab)
- **Restore dashboards:** Add RBAC [#90270](https://github.com/grafana/grafana/pull/90270), [@Clarity-89](https://github.com/Clarity-89)
- **Revert:** Calcs: Update diff percent to be a percent [#91563](https://github.com/grafana/grafana/pull/91563), [@Develer](https://github.com/Develer)
- **SAML:** Add button to generate a certificate and private key (Enterprise)
- **SSO:** Make SAML certificate/private key optional (Enterprise)
- **SearchV2:** Support soft deletion [#90217](https://github.com/grafana/grafana/pull/90217), [@ryantxu](https://github.com/ryantxu)
- **Select:** Add orange indicator to selected item [#88695](https://github.com/grafana/grafana/pull/88695), [@tskarhed](https://github.com/tskarhed)
- **Snapshots:** Remove deprecated option snapshot_remove_expired [#91231](https://github.com/grafana/grafana/pull/91231), [@ryantxu](https://github.com/ryantxu)
- **Table panel:** Add alt and title text options to image cell type [#89930](https://github.com/grafana/grafana/pull/89930), [@codeincarnate](https://github.com/codeincarnate)
- **Tempo:** Add toggle for streaming [#88685](https://github.com/grafana/grafana/pull/88685), [@fabrizio-grafana](https://github.com/fabrizio-grafana)
- **Tempo:** Remove kind=server from metrics summary [#89419](https://github.com/grafana/grafana/pull/89419), [@joey-grafana](https://github.com/joey-grafana)
- **Tempo:** Run `go get` [#89335](https://github.com/grafana/grafana/pull/89335), [@fabrizio-grafana](https://github.com/fabrizio-grafana)
- **Tempo:** TraceQL metrics step option [#89434](https://github.com/grafana/grafana/pull/89434), [@adrapereira](https://github.com/adrapereira)
- **Tempo:** Virtualize tags select to improve performance [#90269](https://github.com/grafana/grafana/pull/90269), [@adrapereira](https://github.com/adrapereira)
- **Tempo:** Virtualized search dropdowns for attribute values [#88569](https://github.com/grafana/grafana/pull/88569), [@RonanQuigley](https://github.com/RonanQuigley)
- **TimePicker:** Improve screen reader support [#89409](https://github.com/grafana/grafana/pull/89409), [@tskarhed](https://github.com/tskarhed)
- **TimeRangePicker:** Add weekStart prop [#89650](https://github.com/grafana/grafana/pull/89650), [@Clarity-89](https://github.com/Clarity-89)
- **TimeRangePicker:** Use week start [#89765](https://github.com/grafana/grafana/pull/89765), [@Clarity-89](https://github.com/Clarity-89)
- **Tooltip:** Add tooltip support to Histogram [#89196](https://github.com/grafana/grafana/pull/89196), [@adela-almasan](https://github.com/adela-almasan)
- **Trace View:** Add Session for this span button [#89656](https://github.com/grafana/grafana/pull/89656), [@javiruiz01](https://github.com/javiruiz01)
- **Tracing:** Add regex support for span filters [#89885](https://github.com/grafana/grafana/pull/89885), [@ektasorathia](https://github.com/ektasorathia)
- **Transformations:** Add variable support to select groupingToMatrix [#88551](https://github.com/grafana/grafana/pull/88551), [@kazeborja](https://github.com/kazeborja)
- **Transformations:** Move transformation variables to general availability [#89111](https://github.com/grafana/grafana/pull/89111), [@samjewell](https://github.com/samjewell)
- **Transformations:** Promote add field from calc stat function cumulative and window calcs as generally available [#91160](https://github.com/grafana/grafana/pull/91160), [@nmarrs](https://github.com/nmarrs)
- **Transformations:** Promote format string as generally available [#91161](https://github.com/grafana/grafana/pull/91161), [@nmarrs](https://github.com/nmarrs)
- **Transformations:** Promote group to nested table as generally available [#90253](https://github.com/grafana/grafana/pull/90253), [@nmarrs](https://github.com/nmarrs)
- **Users:** Add config option to control how often last_seen is updated [#88721](https://github.com/grafana/grafana/pull/88721), [@parambath92](https://github.com/parambath92)
- **XYChart:** Promote to generally available [#91417](https://github.com/grafana/grafana/pull/91417), [@nmarrs](https://github.com/nmarrs)
### Bug fixes
- **Admin:** Fixes logic for enabled a user [#88117](https://github.com/grafana/grafana/pull/88117), [@gonvee](https://github.com/gonvee)
- **Alerting:** Add validation for path separators in the rule group edit modal [#90887](https://github.com/grafana/grafana/pull/90887), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Allow future relative time [#89405](https://github.com/grafana/grafana/pull/89405), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Disable simplified routing when internal alert manager is disabled [#90648](https://github.com/grafana/grafana/pull/90648), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Do not check evaluation interval for external rulers [#89354](https://github.com/grafana/grafana/pull/89354), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Do not count rule health for totals [#89349](https://github.com/grafana/grafana/pull/89349), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Fix Recording Rules creation issues [#90362](https://github.com/grafana/grafana/pull/90362), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Fix contact point export 500 error and notifications/receivers missing settings [#90342](https://github.com/grafana/grafana/pull/90342), [@JacobsonMT](https://github.com/JacobsonMT)
- **Alerting:** Fix permissions for prometheus rule endpoints [#91409](https://github.com/grafana/grafana/pull/91409), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Fix persisting result fingerprint that is used by recovery threshold [#91224](https://github.com/grafana/grafana/pull/91224), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Fix rule storage to filter by group names using case-sensitive comparison [#88992](https://github.com/grafana/grafana/pull/88992), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Fix saving telegram contact point to Cloud AM config [#89182](https://github.com/grafana/grafana/pull/89182), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Fix setting of existing Telegram Chat ID value [#89287](https://github.com/grafana/grafana/pull/89287), [@tomratcliffe](https://github.com/tomratcliffe)
- **Alerting:** Fix silencing from policy instances [#90417](https://github.com/grafana/grafana/pull/90417), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **Alerting:** Fix some status codes returned from provisioning API. [#90117](https://github.com/grafana/grafana/pull/90117), [@stevesg](https://github.com/stevesg)
- **Alerting:** Fix stale values associated with states that have gone to NoData, unify values calculation [#89807](https://github.com/grafana/grafana/pull/89807), [@alexweav](https://github.com/alexweav)
- **Alerting:** Refactor PromQL-style matcher parsing [#90129](https://github.com/grafana/grafana/pull/90129), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Skip fetching alerts for unsaved dashboards [#90061](https://github.com/grafana/grafana/pull/90061), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Skip loading alert rules for dashboards when disabled [#89361](https://github.com/grafana/grafana/pull/89361), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Support `utf8_strict_mode: false` in Mimir [#90092](https://github.com/grafana/grafana/pull/90092), [@gillesdemey](https://github.com/gillesdemey)
- **Alerting:** Time interval Delete API to check for usages in alert rules [#90500](https://github.com/grafana/grafana/pull/90500), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Analytics:** Fix ApplicationInsights integration [#89299](https://github.com/grafana/grafana/pull/89299), [@ashharrison90](https://github.com/ashharrison90)
- **Azure Monitor:** Add validation for namespace field in AdvancedResourcePicker when entering a forward slash [#89288](https://github.com/grafana/grafana/pull/89288), [@adamyeats](https://github.com/adamyeats)
- **AzureMonitor:** Fix out of bounds error when accessing `metricNamespaceArray` and `resourceNameArray` in `buildResourceURI` [#89222](https://github.com/grafana/grafana/pull/89222), [@adamyeats](https://github.com/adamyeats)
- **BrowseDashboards:** Prepend subpath to New Browse Dashboard actions [#89109](https://github.com/grafana/grafana/pull/89109), [@joshhunt](https://github.com/joshhunt)
- **CloudWatch:** Fix labels for raw metric search queries [#88943](https://github.com/grafana/grafana/pull/88943), [@iwysiu](https://github.com/iwysiu)
- **CloudWatch:** Fix raw queries with dimensions set [#90348](https://github.com/grafana/grafana/pull/90348), [@iwysiu](https://github.com/iwysiu)
- **Correlations:** Fix wrong target data source name in the form [#90340](https://github.com/grafana/grafana/pull/90340), [@aocenas](https://github.com/aocenas)
- **DashboardScene:** Fixes issue removing override rule [#89124](https://github.com/grafana/grafana/pull/89124), [@torkelo](https://github.com/torkelo)
- **DashboardScene:** Fixes lack of re-render when updating field override properties [#88796](https://github.com/grafana/grafana/pull/88796), [@torkelo](https://github.com/torkelo)
- **DataSourcePicker:** Create new data source does not work for subpath [#90536](https://github.com/grafana/grafana/pull/90536), [@ivanortegaalba](https://github.com/ivanortegaalba)
- **Docs:** Add fixed role UUIDs to docs for terraform provisioning [#89457](https://github.com/grafana/grafana/pull/89457), [@Jguer](https://github.com/Jguer)
- **Echo:** Suppress errors from frontend-metrics API call failing [#89379](https://github.com/grafana/grafana/pull/89379), [@joshhunt](https://github.com/joshhunt)
- **Explore Metrics:** Implement grouping with metric prefixes [#89481](https://github.com/grafana/grafana/pull/89481), [@itsmylife](https://github.com/itsmylife)
- **Fix:** Portuguese Brazilian wasn't loading translations [#89302](https://github.com/grafana/grafana/pull/89302), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana)
- **Folders:** Fix folder pagination for cloud instances with many folders [#90008](https://github.com/grafana/grafana/pull/90008), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **Folders:** Improve folder move permission checks [#90588](https://github.com/grafana/grafana/pull/90588), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **InfluxDB:** Fix query builder produces invalid SQL query when using wildcard column name [#89032](https://github.com/grafana/grafana/pull/89032), [@wasim-nihal](https://github.com/wasim-nihal)
- **Inspect:** Include only BOM char for excel files [#88994](https://github.com/grafana/grafana/pull/88994), [@ivanortegaalba](https://github.com/ivanortegaalba)
- **Jaeger:** Fix calling of search query with the correct time range [#90320](https://github.com/grafana/grafana/pull/90320), [@EgorKluch](https://github.com/EgorKluch)
- **Metrics:** Fix internal metrics endpoint not accessible from browser if basic auth is enabled [#86904](https://github.com/grafana/grafana/pull/86904), [@wasim-nihal](https://github.com/wasim-nihal)
- **Notifications:** Redact URL from errors [#85687](https://github.com/grafana/grafana/pull/85687), [@alexweav](https://github.com/alexweav)
- **PDF:** Fix layout for page-size panel after row (Enterprise)
- **Panel:** Fix text aliasing bug when panel is loading [#89538](https://github.com/grafana/grafana/pull/89538), [@ashharrison90](https://github.com/ashharrison90)
- **Plugin extensions:** Return react components from `usePluginComponents()` [#89237](https://github.com/grafana/grafana/pull/89237), [@leventebalogh](https://github.com/leventebalogh)
- **Plugins:** Ensure grafana cli can install multiple plugin dependencies [#91230](https://github.com/grafana/grafana/pull/91230), [@yincongcyincong](https://github.com/yincongcyincong)
- **Prometheus:** Fix interpolating adhoc filters with template variables [#88626](https://github.com/grafana/grafana/pull/88626), [@cazeaux](https://github.com/cazeaux)
- **Prometheus:** Fix query builder visualization when a query has by() clause for quantile [#88480](https://github.com/grafana/grafana/pull/88480), [@yuri-rs](https://github.com/yuri-rs)
- **QueryEditor:** Break with Scenes because the default query is not empty string [#90583](https://github.com/grafana/grafana/pull/90583), [@ivanortegaalba](https://github.com/ivanortegaalba)
- **RBAC:** Fix seeder failures when inserting duplicated permissions (Enterprise)
- **RBAC:** List only the folders that the user has access to [#88599](https://github.com/grafana/grafana/pull/88599), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **Scenes/Dashboards:** Fix issue where changes in panel height weren't saved [#91125](https://github.com/grafana/grafana/pull/91125), [@kaydelaney](https://github.com/kaydelaney)
- **Scenes:** Fixes issue with panel repeat height calculation [#90221](https://github.com/grafana/grafana/pull/90221), [@kaydelaney](https://github.com/kaydelaney)
- **Scenes:** Implement 't a' shortcut [#89619](https://github.com/grafana/grafana/pull/89619), [@kaydelaney](https://github.com/kaydelaney)
- **Table Panel:** Fix Image hover without datalinks [#89751](https://github.com/grafana/grafana/pull/89751), [@codeincarnate](https://github.com/codeincarnate)
- **Table component:** Fix sub-table rows not displaying correctly [#89082](https://github.com/grafana/grafana/pull/89082), [@codeincarnate](https://github.com/codeincarnate)
- **Tempo:** Fix grpc streaming support over pdc-agent [#89883](https://github.com/grafana/grafana/pull/89883), [@taylor-s-dean](https://github.com/taylor-s-dean)
- **Tempo:** Fix query history [#89991](https://github.com/grafana/grafana/pull/89991), [@joey-grafana](https://github.com/joey-grafana)
### Breaking changes
- **Folders:** Allow folder editors and admins to create subfolders without any additional permissions [#91215](https://github.com/grafana/grafana/pull/91215), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
### Plugin development fixes & changes
- **Runtime:** Add provider and access hook for location service [#90759](https://github.com/grafana/grafana/pull/90759), [@aocenas](https://github.com/aocenas)
<!-- 11.2.0 END -->
<!-- 11.1.5 START -->
# 11.1.5 (2024-08-27)
### Bug fixes
- **Alerting:** Fix permissions for prometheus rule endpoints [#91414](https://github.com/grafana/grafana/pull/91414), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Alerting:** Fix persisting result fingerprint that is used by recovery threshold [#91290](https://github.com/grafana/grafana/pull/91290), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Auditing:** Fix a possible crash when audit logger parses responses for failed requests (Enterprise)
- **RBAC:** Fix an issue with server admins not being able to manage users in orgs that they don't belong to [#92273](https://github.com/grafana/grafana/pull/92273), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
- **RBAC:** Fix an issue with server admins not being able to manage users in orgs that they dont belong to (Enterprise)
- **RBAC:** Fix seeder failures when inserting duplicated permissions (Enterprise)
- **Snapshots:** Fix panic when snapshot_remove_expired is true [#91232](https://github.com/grafana/grafana/pull/91232), [@ryantxu](https://github.com/ryantxu)
- **VizTooltip:** Fix positioning at bottom and right edges on mobile [#92137](https://github.com/grafana/grafana/pull/92137), [@leeoniya](https://github.com/leeoniya)
### Plugin development fixes & changes
- **Bugfix:** QueryField typeahead missing background color [#92316](https://github.com/grafana/grafana/pull/92316), [@mckn](https://github.com/mckn)
<!-- 11.1.5 END -->
<!-- 11.0.4 START -->
# 11.0.4 (2024-08-27)
### Bug fixes
- **Alerting:** Fix persisting result fingerprint that is used by recovery threshold [#91328](https://github.com/grafana/grafana/pull/91328), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Auditing:** Fix a possible crash when audit logger parses responses for failed requests (Enterprise)
- **RBAC:** Fix seeder failures when inserting duplicated permissions (Enterprise)
- **Snapshots:** Fix panic when snapshot_remove_expired is true [#91330](https://github.com/grafana/grafana/pull/91330), [@ryantxu](https://github.com/ryantxu)
<!-- 11.0.4 END -->
<!-- 10.4.8 START -->
# 10.4.8 (2024-08-27)
### Bug fixes
- **Alerting:** Fix persisting result fingerprint that is used by recovery threshold [#91331](https://github.com/grafana/grafana/pull/91331), [@yuri-tceretian](https://github.com/yuri-tceretian)
- **Auditing:** Fix a possible crash when audit logger parses responses for failed requests (Enterprise)
- **RBAC:** Fix seeder failures when inserting duplicated permissions (Enterprise)
- **Snapshots:** Fix panic when snapshot_remove_expired is true [#91329](https://github.com/grafana/grafana/pull/91329), [@ryantxu](https://github.com/ryantxu)
<!-- 10.4.8 END -->
<!-- 10.3.9 START -->
# 10.3.9 (2024-08-27)
<!-- 10.3.9 END -->
<!-- 11.1.4 START -->
# 11.1.4 (2024-08-14)

View File

@ -211,7 +211,7 @@ grafana cli admin
### Reset admin password
`grafana cli admin reset-admin-password <new password>` resets the password for the admin user using the CLI. You might need to do this if you lose the admin password.
`grafana cli admin reset-admin-password <new password>` resets the password for the admin user using the CLI. You might need to do this if you lose the admin password. By default, this command uses the default ID of the admin user, which is 1. If you know the ID of the admin user, you can use the `--user-id` flag to specify the user ID. If the `--user-id` flag is not specified and the command cannot find the admin user, it returns the list of current admin users' username and ID. From that list you can determine the ID of the admin user and run the command again with the `--user-id` flag.
If there are two flags being used to set the homepath and the config file path, then running the command returns this error:
@ -227,6 +227,14 @@ If you have not lost the admin password, we recommend that you change the user p
If you need to set the password in a script, then you can use the [Grafana User API]({{< relref "./developers/http_api/user/#change-password" >}}).
#### Reset admin password
If you installed Grafana using Homebrew, you can reset the admin password using the following command:
```bash
/opt/homebrew/opt/grafana/bin/grafana cli --config /opt/homebrew/etc/grafana/grafana.ini --homepath /opt/homebrew/opt/grafana/share/grafana --configOverrides cfg:default.paths.data=/opt/homebrew/var/lib/grafana admin reset-admin-password <new password>
```
### Migrate data and encrypt passwords
`data-migration` runs a script that migrates or cleans up data in your database.

View File

@ -26,86 +26,194 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
service-graph:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/tempo/service-graph/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/service-graph/
recorded-queries:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/recorded-queries/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/recorded-queries/
query-history-management:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/query-management/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/query-management/
query-inspector:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/explore-inspector/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/explore-inspector/
---
# Query tracing data
The Tempo data source's query editor helps you query and display traces from Tempo in [Explore](ref:explore).
The queries use [TraceQL](/docs/tempo/latest/traceql), the query language designed specifically for tracing.
This topic explains configuration and queries specific to the Tempo data source.
For general documentation on querying data sources in Grafana, see [Query and transform data](ref:query-transform-data).
For general documentation on querying data sources in Grafana, refer to [Query and transform data](ref:query-transform-data).
## Before you begin
You can compose TraceQL queries in Grafana and Grafana Cloud using **Explore** and a Tempo data source.
### TraceQL knowledge helpful, but not required
You don't have to know TraceQL to create a query.
You can use the **Search** query builder's user interface to select options to search your data.
These selections generate a TraceQL query.
Any query generated using **Search** query builder can be transferred to the **TraceQL** query editor, where you can edit the query directly.
To learn more about how to query by TraceQL, refer to the [TraceQL documentation](/docs/tempo/latest/traceql).
## Choose a query editing mode
The query editor has three **Query types** that you can use to explore your tracing data.
You can use these modes by themselves or in combination to create building blocks to generate custom queries.
Adding another query adds a new query block.
Refer to [Use query types together](#use-query-types-together) for more information.
![The three query types: Search, TraceQL, and Service Graph](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-types.png)
The three query types are:
- **Search** query builder - Provides a user interface for building a TraceQL query.
- **TraceQL** query editor - Lets you write your own TraceQL query with assistance from autocomplete.
- **Service Graph** view - Displays a visual relationship between services. Refer to the [Service graph](ref:service-graph) documentation for more information.
### Search query builder
The **Search** query builder provides drop-down lists and text fields to help you write a query.
The query builder is ideal for people who aren't familiar with or want to learn TraceQL.
Refer to the [Search using the TraceQL query builder documentation]({{< relref "./traceql-search" >}}) to learn more about creating queries using convenient drop-down menus.
![The Search query builder](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-search-v11.png)
### TraceQL query editor
The **TraceQL** query editor lets you search by trace ID and write TraceQL queries using autocomplete.
Refer to the [TraceQL query editor documentation]({{< relref "./traceql-editor" >}}) to learn more about constructing queries using a code-editor-like experience.
![The TraceQL query editor](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-traceql-v11.png)
You can also search for a trace ID by entering it into the query field.
### Service graph view
Grafanas **Service Graph** view uses metrics to display span request rates, error rates, and durations, as well as service graphs.
Once the requirements are set up, this preconfigured view is immediately available.
Using the service graph view, you can:
- Discover spans which are consistently erroring and the rates at which they occur.
- Get an overview of the overall rate of span calls throughout your services.
- Determine how long the slowest queries in your service take to complete.
- Examine all traces that contain spans of particular interest based on rate, error, and duration values (RED signals).
For more information about the service graph, refer to [Service graph](../service-graph/).
![Screenshot of the Service Graph view](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-service-graph.png)
## Use TraceQL panels in dashboards
To add TraceQL panels to your dashboard, refer to the [Traces panel documentation](/docs/grafana/latest/panels-visualizations/visualizations/traces/).
To learn more about Grafana dashboards, refer to the [Use dashboards documentation](/docs/grafana/latest/dashboards/use-dashboards/).
## Write TraceQL queries in Grafana
## Set options for query builder and editor
You can compose TraceQL queries in Grafana and Grafana Cloud using **Explore** and a Tempo data source. You can use either the **Query type** > **Search** (the TraceQL query builder) or the **TraceQL** tab (the TraceQL query editor).
Both of these methods let you build queries and drill-down into result sets.
The following options are available for the **Search** and **TraceQL** query types.
You can modify these settings in the **Options** section.
To learn more about how to query by TraceQL, refer to the [TraceQL documentation](/docs/tempo/latest/traceql).
![Options section in the query editors](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-options.png)
### TraceQL query builder
After changing any option, re-run the query to apply the updates.
The TraceQL query builder, located on the **Explore** > **Query type** > **Search** in Grafana, provides drop-downs and text fields to help you write a query.
Limit
: Determines the maximum number of traces to return. Default value is `20`.
Refer to the [Search using the TraceQL query builder documentation]({{< relref "./traceql-search" >}}) to learn more about creating queries using convenient drop-down menus.
Span Limit
: Sets the maximum number of spans to return for each spanset. Default value is `3`.
![The TraceQL query builder](/static/img/docs/tempo/screenshot-traceql-query-type-search-v10.png)
Table Format
: Determines whether the query results table is displayed focused on **Traces** or **Spans**. **Traces** is the default selection. When **Traces** is selected, the results table starts with the trace ID. When **Spans** is selected, the table starts with the trace service.
### TraceQL query editor
Step
: Defines the step for metrics queries. Use duration notation, for example, `30ms` or `1m`.
The TraceQL query editor, located on the **Explore** > **TraceQL** tab in Grafana, lets you search by trace ID and write TraceQL queries using autocomplete.
Streaming
: Indicates if streaming is active. Streaming lets you view partial query results before the entire query completes. Activating streaming adds the **Table - Streaming Progress** section to the query results.
Refer to the [TraceQL query editor documentation]({{< relref "./traceql-editor" >}}) to learn more about constructing queries using a code-editor-like experience.
## Use query types together
![The TraceQL query editor](/static/img/docs/tempo/screenshot-traceql-query-editor-v10.png)
You can use **+ Add query** to create customized queries that use one or more of the query types together.
Each time you add a new query, it adds a new section, or query block, that contains **Search**, **TraceQL**, or **Service Graph** user interface.
## Query by search (deprecated)
The added query and results table appear in the navigation under **Queries** and **Tables** respectively.
You can use the navigation to view query, results table, and service graph blocks.
{{% admonition type="caution" %}}
Starting with Grafana v10.2, this query type has been deprecated. It will be removed in Grafana v10.3.
{{% /admonition %}}
{{< video-embed src="/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-editor.mp4" max-width="800px" class="my-cool-video" caption="Navigating through the query blocks" align="center" >}}
Use this to search for traces by service name, span name, duration range, or process-level attributes that are included in your application's instrumentation, such as HTTP status code and customer ID.
To add a query block:
To configure Tempo and the Tempo data source for search, refer to [Configure the data source]({{< relref "../#configure-the-data-source" >}}).
1. Select **+ Add query**.
1. Choose a query type: **Search**, **TraceQL**, or **Service Graph**.
To search for traces:
To remove a query block, select the **Remove query** trash can icon.
1. Select **Search** from the **Query** type selector.
1. Fill out the search form:
To rename a block, select the **Rename** edit icon next to the query block name.
The name changes in the queries and table list.
| Name | Description |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Service Name** | Returns a list of services. |
| **Span Name** | Returns a list of span names. |
| **Tags** | Sets tags with values in the [logfmt](https://brandur.org/logfmt) format, such as `error=true db.statement="select * from User"`. |
| **Min Duration** | Filters all traces with a duration higher than the set value. Possible values are `1.2s`, `100ms`, `500us`. |
| **Max Duration** | Filters all traces with a duration lower than the set value. Possible values are `1.2s`, `100ms`, `500us`. |
| **Limit** | Limits the number of traces returned. |
### Additional query block options
{{< figure src="/static/img/docs/explore/tempo-search.png" class="docs-image--no-shadow" max-width="750px" caption="Screenshot of the Tempo search feature with a trace rendered in the right panel" >}}
Each query block has a set of icons in the right top corner.
### Search recent traces
![The additional options toolbar](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-additional-options-toolbar.png)
You can search recent traces held in Tempo's ingesters.
By default, ingesters store the last 15 minutes of tracing data.
These icons include:
To configure your Tempo data source to use this feature, refer to the [Tempo documentation](/docs/tempo/latest/getting-started/tempo-in-grafana/#search-of-recent-traces).
Show data source help
: Displays the **Tempo Cheat Sheet** with links to documentation.
### Search the backend datastore
Create recorded query
: Lets you save the current query block as a recorded query. This option is available in Grafana Cloud and Grafana Enterprise. For more information, refer to [Recorded queries](ref:recorded-queries).
Tempo includes the ability to search the entire backend datastore.
Duplicate query
: Copies the current block and adds a new identical block.
To configure your Tempo data source to use this feature, refer to the [Tempo documentation](/docs/tempo/latest/getting-started/tempo-in-grafana/#search-of-the-backend-datastore).
Remove query
: Deletes the query block.
## Query by TraceID
### Use query history and query inspector
To query a particular trace:
**Explore** provides a history of all queries you've used within a data source and an inspector that lets you view stats, inspect queries, view JSON, and general information for your data source queries.
1. Select the **TraceQL** query type.
1. Enter the trace's ID into the query field.
For more information, refer to the [Query inspector in Explore](ref:query-inspector) and [Query management in Explore](ref:query-history-management) documentation.
{{< figure src="/static/img/docs/tempo/query-editor-traceid.png" class="docs-image--no-shadow" max-width="750px" caption="Screenshot of the Tempo TraceID query type" >}}
## Cross-tenant TraceQL queries
If you've configured a multi-stack Tempo data source, you can perform TraceQL queries across those stacks and tenants.
Queries performed using the cross-tenant configured data source, in either **Explore** or inside of dashboards,
are performed across all the tenants that you specified in the **X-Scope-OrgID** header.
<!-- vale Grafana.Spelling = NO -->
TraceQL queries that compare multiple spansets may not correctly return all traces in a cross-tenant query. For instance,
<!-- vale Grafana.Quotes = YES -->
```
{ span.attr1 = "bar" } && { span.attr2 = "foo" }
```
TraceQL evaluates a contiguously stored trace.
If these two conditions are satisfied in separate tenants, then Tempo doesn't return the trace.
Refer to [Set up a multi-stack Tempo data source in Grafana](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/multi-stack-data-sources/#set-up-a-multi-stack-tempo-data-source-in-grafana) for information about configuring the Tempo data source.
For information about Tempo configuration requirements, refer to the [Cross-tenant query](https://grafana.com/docs/tempo/<TEMPO_VERSION>/operations/cross_tenant_query/) and [Enable multitenancy](https://grafana.com/docs/tempo/<TEMPO_VERSION>/operations/multitenancy/) documentation.

View File

@ -13,6 +13,27 @@ labels:
menuTitle: Write TraceQL queries
title: Write TraceQL queries with the editor
weight: 300
refs:
explore:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
service-graph:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/tempo/service-graph/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/service-graph/
recorded-queries:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/recorded-queries/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/recorded-queries/
tempo-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/tempo/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/query-editor/
---
# Write TraceQL queries with the editor

View File

@ -11,11 +11,32 @@ labels:
- enterprise
- oss
menuTitle: Search traces
title: Search traces using TraceQL query builder
title: Investigate traces using Search query builder
weight: 300
refs:
explore:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
service-graph:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/tempo/service-graph/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/service-graph/
recorded-queries:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/recorded-queries/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/recorded-queries/
tempo-query-editor:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/tempo/query-editor/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/query-editor/
---
# Search traces using TraceQL query builder
# Investigate traces using Search query builder
Inspired by PromQL and LogQL, TraceQL is a query language designed for selecting traces.
TraceQL provides a method for formulating precise queries so you can zoom in to the data you need.
@ -23,9 +44,9 @@ Query results are returned faster because the queries limit what is searched.
To learn more about how to query by TraceQL, refer to the [TraceQL documentation](/docs/tempo/latest/traceql).
The TraceQL query builder, located on the **Explore** > **Query type** > **Search** in Grafana, provides drop-downs and text fields to help you write a query.
The **Search** query builder, located on the **Explore** > **Query type** > **Search** in Grafana, provides drop-down lists and text fields to help you write a query.
![The TraceQL query builder](/static/img/docs/tempo/screenshot-traceql-query-type-search-v10.png)
![The Search query builder](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-builder-v11.png)
## Enable Search with the query builder

View File

@ -68,7 +68,7 @@ Each node on the graph represents a service such as an API or database.
You use the Service Graph to detect performance issues; track increases in error, fault, or throttle rates in services; and investigate root causes by viewing corresponding traces.
{{< figure src="/static/img/docs/node-graph/node-graph-8-0.png" class="docs-image--no-shadow" max-width="500px" caption="Screenshot of a Node Graph" >}}
{{< figure src="/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-node-graph.png" class="docs-image--no-shadow" max-width="500px" alt="Screenshot of a Node Graph" >}}
## Display the Service Graph
@ -100,9 +100,9 @@ Each circle's color represents the percentage of requests in each state:
Service graph view displays a table of request rate, error rate, and duration metrics (RED) calculated from your incoming spans. It also includes a node graph view built from your spans.
{{< figure src="/static/img/docs/tempo/apm-table.png" class="docs-image--no-shadow" max-width="500px" caption="Screenshot of the Service Graph view" >}}
{{< figure src="/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-service-graph.png" class="docs-image--no-shadow" max-width="500px" alt="Screenshot of the Service Graph view" >}}
For details, refer to the [Service Graph view documentation](/docs/tempo/latest/metrics-generator/service-graph-view/).
For details, refer to the [Service Graph view documentation](/docs/tempo/<TEMPO_VERSION>/metrics-generator/service-graph-view/).
To open the Service Graph view:
@ -120,4 +120,6 @@ These metrics must exist in your Prometheus data source.
To open a query in Prometheus with the span name of that row automatically set in the query, click a row in the **rate**, **error rate**, or **duration** columns.
![Linked Prometheus data for Rate from within a service graph](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-service-graph-prom.png)
To open a query in Tempo with the span name of that row automatically set in the query, click a row in the **links** column.

View File

@ -29,6 +29,11 @@ If you use Grafana v9.1 or newer, use service accounts instead of API keys. For
## List API keys
{{% admonition type="warning" %}}
This endpoint is deprecated.
{{% /admonition %}}
`GET /api/auth/keys`
**Required permissions**
@ -75,6 +80,13 @@ Content-Type: application/json
## Create API Key
{{% admonition type="warning" %}}
This endpoint has been made obsolete in Grafana 11.3.0.
{{% /admonition %}}
Endpoint is obsolete and has been moved to [Grafana service account API]({{< relref "./serviceaccount/" >}}). For more information, refer to [Migrate to Grafana service account API]({{< relref "../../administration/api-keys/#migrate-api-keys-to-grafana-service-accounts-using-the-api" >}}).
`POST /api/auth/keys`
**Required permissions**
@ -114,14 +126,20 @@ Error statuses:
**Example Response**:
```http
HTTP/1.1 200
HTTP/1.1 301
Content-Type: application/json
{"name":"mykey","key":"eyJrIjoiWHZiSWd3NzdCYUZnNUtibE9obUpESmE3bzJYNDRIc0UiLCJuIjoibXlrZXkiLCJpZCI6MX1=","id":1}
""
```
## Delete API Key
{{% admonition type="warning" %}}
### DEPRECATED
{{% /admonition %}}
`DELETE /api/auth/keys/:id`
**Required permissions**

View File

@ -0,0 +1,18 @@
---
description: Use your telemetry data to explore and determine the root cause of issues without performing queries.
keywords:
- Simplified exploration
- queryless
- Explore apps
title: Simplified exploration
menuTitle: Simplified exploration
weight: 100
---
# Simplified exploration
Introducing the Grafana Explore apps, designed for effortless data exploration through intuitive, queryless interactions.
Easily explore telemetry signals with these specialized tools, tailored specifically for the Grafana databases to provide quick and accurate insights.
{{< section >}}

View File

@ -6,7 +6,8 @@ labels:
- oss
title: Explore Metrics
aliases:
canonical: https://grafana.com/docs/grafana/latest/explore/explore-metrics/
- ../explore-metrics/ # /docs/grafana/latest/explore/explore-metrics/
canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/metrics/
description: Explore Metrics lets you browse Prometheus-compatible metrics using an intuitive, queryless experience.
weight: 200
---

View File

@ -98,6 +98,7 @@ This option only applies when the orientation of the bar gauge is horizontal. Wh
- **Auto -** Grafana determines the best placement.
- **Top -** Names are placed on top of each bar gauge.
- **Left -** Names are placed to the left of each bar gauge.
- **Hidden -** Names are hidden on each bar gauge.
### Show unfilled area

View File

@ -49,13 +49,28 @@ refs:
# Geomap
Geomaps allow you to view and customize the world map using geospatial data. You can configure various overlay styles and map view settings to easily focus on the important location-based characteristics of the data.
Geomaps allow you to view and customize the world map using geospatial data. It's the ideal visualization if you have data that includes location information and you want to see it displayed in a map.
> We would love your feedback on geomaps. Please check out the [open Github issues](https://github.com/grafana/grafana/issues?page=1&q=is%3Aopen+is%3Aissue+label%3Aarea%2Fpanel%2Fgeomap) and [submit a new feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request,area%2Fpanel%2Fgeomap&title=Geomap:&projects=grafana-dataviz&template=1-feature_requests.md) as needed.
You can configure and overlay [map layers](#types), like heatmaps and networks, and blend included basemaps or your own custom maps. This helps you to easily focus on the important location-based characteristics of the data.
{{< figure src="/static/img/docs/geomap-panel/geomap-example-8-1-0.png" max-width="1200px" caption="Geomap panel" >}}
{{< figure src="/static/img/docs/geomap-panel/geomap-example-8-1-0.png" max-width="1200px" alt="Geomap visualization" >}}
Pan the map, while it's in focus, by using the arrow keys. Zoom in and out by using the `+` and `-` keys.
When a geomap is in focus, in addition to typical mouse controls, you can pan around using the arrow keys or zoom in and out using the plus (`+`) and minus (`-`) keys or icons.
Geomaps are also useful when you have location data thats changing in real time and you want to visualize where an element is moving, using auto-refresh.
You can use a geomap visualization if you need to:
- Track your fleet of vehicles and associated metrics
- Show the locations and statuses of data centers or other connected assets in a network
- Display geographic trends in a heatmap
- Visualize the relationship of your locations' HVAC consumption or solar production with the sun's location
{{< admonition type="note" >}}
We'd love your feedback on the geomap visualization. Please check out the [open Github issues](https://github.com/grafana/grafana/issues?page=1&q=is%3Aopen+is%3Aissue+label%3Aarea%2Fpanel%2Fgeomap) and [submit a new feature request](https://github.com/grafana/grafana/issues/new?assignees=&labels=type%2Ffeature-request,area%2Fpanel%2Fgeomap&title=Geomap:&projects=grafana-dataviz&template=1-feature_requests.md) as needed.
{{< /admonition >}}
## Configure a geomap visualization
The following video provides beginner steps for creating geomap visualizations. You'll learn the data requirements and caveats, special customizations, preconfigured displays and much more:
@ -63,6 +78,69 @@ The following video provides beginner steps for creating geomap visualizations.
{{< docs/play title="Geomap Examples" url="https://play.grafana.org/d/panel-geomap/" >}}
## Supported data formats
To create a geomap visualization, you need datasets containing fields with location information.
The supported location formats are:
- Latitude and longitude
- Geohash
- Lookup codes: country, US states, or airports
To learn more, refer to [Location mode](#location-mode).
Geomaps also support additional fields with various data types to define things like labels, numbers, heat sizes, and colors.
### Example - Latitude and longitude
If you plan to use latitude and longitude coordinates, the dataset must include at least two fields (or columns): one called `latitude` (you can also use`lat`), and one called `longitude` (also `lon` or `lng`). When you use this naming convention, the visualization automatically detects the fields and displays the elements. The order of the fields doesn't matter as long as there is one latitude and one longitude.
| Name | latitude | longitude | value |
| --------------- | --------- | --------- | ----- |
| Disneyland | 33.8121 | -117.9190 | 4 |
| DisneyWorld | 28.3772 | -81.5707 | 10 |
| EuroDisney | 48.867374 | 2.784018 | 3 |
| Tokyo Disney | 35.6329 | 139.8804 | 70 |
| Shanghai Disney | 31.1414 | 121.6682 | 1 |
If your latitude and longitude fields are named differently, you can specify them, as indicated in the [Location mode](#location-mode) section.
### Example - Geohash
If your location data is in geohash format, the visualization requires at least one field (or column) containing location data.
If the field is named `geohash`, the visualization automatically detects the location and displays the elements. The order of the fields doesn't matter and the data set can have multiple other numeric, text, and time fields.
| Name | geohash | trips |
| --------- | ------------ | ----- |
| Cancun | d5f21 | 8 |
| Honolulu | 87z9ps | 0 |
| Palm Cove | rhzxudynb014 | 1 |
| Mykonos | swdj02ey9gyx | 3 |
If your field containing geohash location data is not named as above, you can configure the visualization to use geohash and specify which field to use, as explained in the [Location mode](#location-mode) section.
### Example - Lookup codes
The geomap visualization can identify locations based on country, airport, or US state codes.
For this configuration, the dataset must contain at least one field (or column) containing the location code.
If the field is named `lookup`, the visualization automatically detects it and displays points based on country codes.
| Year | lookup | gdp |
| ---- | ------ | --------- |
| 2016 | MEX | 104171935 |
| 2016 | DEU | 94393454 |
| 2016 | FRA | 83654250 |
| 2016 | BRA | 80921527 |
| 2016 | CAN | 79699762 |
The other location types&mdash; airport codes or US state codes&mdash;aren't automatically detected.
If you want to use other codes or give the field a custom name, you can follow the steps in the [Location mode](#location-mode) section.
## Panel options
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}

View File

@ -19,7 +19,7 @@ description: Configure options for Grafana's logs visualization
title: Logs
weight: 100
refs:
supported-log-levels-and-mappings-of-log-level-abbreviation-and-expressions:
log-levels:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/explore/logs-integration/#log-level
- pattern: /docs/grafana-cloud/
@ -28,27 +28,51 @@ refs:
# Logs
The logs visualization shows log lines from data sources that support logs, such as Elastic, Influx, and Loki. Typically you would use this visualization next to a graph visualization to display the log output of a related process.
_Logs_ are structured records of events or messages generated by a system or application&mdash;that is, a series of text records with status updates from your system or app. They generally include timestamps, messages, and context information like the severity of the logged event.
The logs visualization displays these records from data sources that support logs, such as Elastic, Influx, and Loki. The logs visualization has colored indicators of log status, as well as collapsible log events that help you analyze the information generated.
{{< figure src="/static/img/docs/v64/logs-panel.png" max-width="1025px" alt="Logs panel" >}}
{{< docs/play title="Logs Panel" url="https://play.grafana.org/d/6NmftOxZz/" >}}
The logs visualization shows the result of queries that were entered in the Query tab. The results of multiple queries are merged and sorted by time. You can scroll inside the panel if the data source returns more lines than can be displayed at any one time.
Typically, you use logs with a graph visualization to display the log output of a related process. If you have an incident in your application or systems, such as a website disruption or code failure, you can use the logs visualization to help you figure out what went wrong, when, and even why.
To limit the number of lines rendered, you can use the **Max data points** setting in the **Query options**. If it is not set, then the data source will usually enforce a default limit.
## Configure a log visualization
The following video provides a walkthrough of creating a logs visualization. You'll also learn how to customize some settings and log visualization caveats:
{{< youtube id="jSSi_x-fD_8" >}}
## Supported data formats
The logs visualization works best with log-type datasets such as queries from data sources like Loki, Elastic, and InlfuxDB.
You can also build log-formatted data from other data sources as long as the first field is a time type followed by string, number, and time fields. The leading time field is used to sort and timestamp the logs and if the data contains other time-type fields, theyre included as elements of the logged record.
The second field is used as the log record title regardless of whether its a time, numeric, or string field. Usually the second field is a text field containing multiple string elements, but if the message level (or `lvl`) is present, the visualization uses the values in it to add colors to the record, as described in [Log levels integration](ref:log-levels).
Subsequent fields are collapsed inside of each log record and you can open them by clicking the expand (`>`) icon.
To limit the number of log lines rendered in the visualization, you can use the **Max data points** setting in the panel **Query options**. If that option isn't set, then the data source typically enforces its own default limit.
### Example
| Time | TitleMessage | Element1 | Element2 | Element3 |
| ------------------- | -------------------- | -------- | -------- | ------------------- |
| 2023-02-01 12:00:00 | title=Log1 lvl=info | 1 | server2 | 2023-02-01 11:00:00 |
| 2023-02-01 11:30:00 | title=Log1 lvl=error | 1 | server2 | 2023-02-01 11:00:00 |
| 2023-02-01 11:00:00 | title=Log1 lvl=trace | 1 | server2 | 2023-02-01 11:00:00 |
![Logs Example](/media/docs/grafana/panels-visualizations/screenshot-grafana-12.1-logs-example.png 'Logs Example')
## Panel options
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
## Log level
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions](ref:supported-log-levels-and-mappings-of-log-level-abbreviation-and-expressions).
For logs where a **level** label is specified, we use the value of the label to determine the log level and update color accordingly. If the log doesn't have a level label specified, we try to find out if its content matches any of the supported expressions (see below for more information). The log level is always determined by the first match. In case Grafana is not able to determine a log level, it will be visualized with **unknown** log level. See [supported log levels and mappings of log level abbreviation and expressions](ref:log-levels).
## Log details

View File

@ -21,9 +21,23 @@ weight: 100
# Node graph
Node graphs can visualize directed graphs or networks. They use a directed force layout to effectively position the nodes, so they can display complex infrastructure maps, hierarchies, or execution diagrams.
Node graphs are useful when you need to visualize elements that are related to each other. This is done by displaying circles&mdash;or _nodes_&mdash;for each element you want to visualize, connected by lines&mdash;or _edges_. The visualization uses a directed force layout that positions the nodes into a network of connected circles.
![Node graph visualization](/static/img/docs/node-graph/node-graph-8-0.png 'Node graph')
Node graphs display useful information about each node, as well as the relationships between them, allowing you to visualize complex infrastructure maps, hierarchies, or execution diagrams.
![Node graph visualization](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-node-graph.png 'Node graph')
The appearance of nodes and edges can also be customized in several ways including color, borders, and line style.
You can use a node graph visualization if you need to show:
- Solution topologies
- Networks
- Infrastructure
- Organizational charts
- Critical path diagrams
- Family trees
- Mind maps
## Configure a node graph visualization
@ -33,6 +47,38 @@ The following video provides beginner steps for creating node panel visualizatio
{{< docs/play title="Node graph panel" url="https://play.grafana.org/d/bdodfbi3d57uoe/" >}}
## Supported data formats
To create node graphs, you need two datasets: one containing the records for the displayed elements (nodes) and one dataset containing the records for the connections between those elements (edges).
### Nodes dataset
The nodes dataset must contain one alphanumeric ID field that gives each element a unique identifier. The visualization also accepts other options fields for titles, subtitles, main and secondary stats, arc information for how much of the circle border to paint, details, colors, icons, node size, and indicators for element highlighting. For more information and naming conventions for these fields, refer to the [Nodes data frame structure](#nodes-data-frame-structure) section.
#### Example
| id | title | subtitle | mainstat | secondarystat | color | icon | highlighted |
| ----- | ----- | -------- | -------- | ------------- | ----- | ---- | ----------- |
| node1 | PC | Windows | AMD | 16gbRAM | blue | | true |
| node2 | PC | Linux | Intel | 32gbRAM | green | eye | false |
| node3 | Mac | MacOS | M3 | 16gbRAM | gray | apps | false |
| node4 | Alone | SoLonely | JustHere | NotConnected | red | | false |
If the icon field contains a value, its displayed instead of the title and subtitle. For a list of of available icons, refer to [Icons Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview).
### Edges dataset
Similar to the nodes dataset, the edges dataset needs one unique ID field for each relationship, followed by two fields containing the source and the target nodes of the edge; that is, the nodes the edge connects. Other optional fields are main and secondary stats, context menu elements, line thickness, highlight indications, line colors, and configurations to turn the connection into a dashed line. For more information and naming conventions for these fields, refer to the [Edges data frame structure](#edges-data-frame-structure) section.
#### Example
| id | source | target | mainstat | seconddarystat | thickness | highlighted | color |
| ----- | ------ | ------ | -------- | -------------- | --------- | ----------- | ------ |
| edge1 | node1 | node2 | TheMain | TheSub | 3 | true | cyan |
| edge2 | node3 | node2 | Main2 | Sub2 | 1 | false | orange |
If a node lacks edge connections, its displayed on its own outside of the network.
## Panel options
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
@ -75,9 +121,11 @@ Node graphs can show only 1,500 nodes. If this limit is crossed a warning will b
Usually, nodes show two statistical values inside the node and two identifiers just below the node, usually name and type. Nodes can also show another set of values as a color circle around the node, with sections of different color represents different values that should add up to 1.
For example, you can have the percentage of errors represented by a red portion of the circle. Additional details can be displayed in a context menu which is displayed when you click on the node. There also can be additional links in the context menu that can target either other parts of Grafana or any external link.
For example, you can have the percentage of errors represented by a red portion of the circle.
Additional details can be displayed in a context menu which is displayed when you click on the node.
There also can be additional links in the context menu that can target either other parts of Grafana or any external link.
![Node graph navigation](/static/img/docs/node-graph/node-graph-navigation-7-4.gif 'Node graph navigation')
![Node graph navigation](/media/docs/grafana/data-sources/tempo/query-editor/node-graph-navigation.png 'Node graph navigation')
### Edges
@ -107,7 +155,9 @@ The number of nodes shown at a given time is limited to maintain a reasonable vi
You can switch to the grid view to have a better overview of the most interesting nodes in the graph. Grid view shows nodes in a grid without edges and can be sorted by stats shown inside the node or by stats represented by the a colored border of the nodes.
![Node graph grid](/static/img/docs/node-graph/node-graph-grid-8-0.png 'Node graph grid')
<!-- Screenshot from v11.2 -->
![Node graph grid](/media/docs/grafana/data-sources/tempo/query-editor/node-graph-grid-view.png 'Node graph grid')
To sort the nodes, click on the stats inside the legend. The marker next to the stat name shows which stat is currently used for sorting and sorting direction.

View File

@ -122,6 +122,7 @@ Experimental features might be changed or removed without prior notice.
| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown |
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
| `mysqlParseTime` | Ensure the parseTime flag is set for MySQL driver |
| `alertingBacktesting` | Rule backtesting API for alerting |
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries |

View File

@ -45,6 +45,22 @@ To install Grafana on macOS using Homebrew, complete the following steps:
brew services start grafana
```
### Using the Grafana CLI with Homebrew
To use the Grafana CLI with Homebrew, you need to append the home path, the config file path and - based on the command - some other configurations to the `cli` command:
For `admin` commands, you need to append the `--configOverrides cfg:default.paths.data=/opt/homebrew/var/lib/grafana` configuration. Example:
```bash
/opt/homebrew/opt/grafana/bin/grafana cli --config /opt/homebrew/etc/grafana/grafana.ini --homepath /opt/homebrew/opt/grafana/share/grafana --configOverrides cfg:default.paths.data=/opt/homebrew/var/lib/grafana admin reset-admin-password <new password>
```
For `plugins` commands, you need to append the `--pluginsDir /opt/homebrew/var/lib/grafana/plugins` configuration. Example:
```bash
/opt/homebrew/opt/grafana/bin/grafana cli --config /opt/homebrew/etc/grafana/grafana.ini --homepath /opt/homebrew/opt/grafana/share/grafana --pluginsDir "/opt/homebrew/var/lib/grafana/plugins" plugins install <plugin-id>
```
## Install standalone macOS binaries
To install Grafana on macOS using the standalone binaries, complete the following steps:

View File

@ -23,19 +23,24 @@ Query results are returned faster because the queries limit what is searched.
To learn more about how to query by TraceQL, refer to the [TraceQL documentation](https://grafana.com/docs/tempo/latest/traceql/).
The TraceQL query editor, located on the **Explore** > **TraceQL** tab in Grafana, lets you search by trace ID and write TraceQL queries using autocomplete.
The TraceQL query editor in Grafana **Explore** lets you search by trace ID and write TraceQL queries using autocomplete.
![The TraceQL query editor](/media/docs/tempo/traceql/screenshot-traceql-query-editor-v10.png)
![The TraceQL query editor](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-traceql-v11.png)
## Enable the query editor
## Before you begin
This feature is automatically available in Grafana 10 (and newer) and Grafana Cloud.
To use the TraceQL query editor in self-hosted Grafana 9.3.2 and older, you need to [enable the `traceqlEditor` feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/).
If trying to query a self-managed Grafana Tempo or Grafana Enterprise Traces database with a gateway (e.g., nginx) in front of it from your hosted Grafana, that gateway (e.g., nginx) must allow gRPC connections. If it does not, streaming will not work and queries will fail to return results.
### Streaming and gRPC
If you cannot configure your gateway to allow gRPC, open a support escalation to request streaming query results be disabled in your hosted Grafana.
If you're trying to query a self-managed Grafana Tempo or Grafana Enterprise Traces database with a gateway, such as nginx, in front of it from your hosted Grafana, that gateway (for example, nginx) must allow gRPC connections.
If it doesn't, streaming won't work and queries will fail to return results.
If you can't configure your gateway to allow gRPC, deactivate streaming in your hosted Grafana.
In Grafana 11.2 and newer, you can deactivate the **Streaming** option in your Tempo data source settings from **Connections** > **Data sources** in the Grafana main menu.
You can also open a support escalation to request streaming query results be disabled in your hosted Grafana.
## Write TraceQL queries using the query editor
@ -44,13 +49,19 @@ The Tempo data sources TraceQL query editor helps you query and display trace
To access the query editor, follow these steps:
1. Sign into Grafana or Grafana Cloud.
1. Select your Tempo data source.
1. From the menu, choose **Explore** and select the **TraceQL** tab.
1. Select **Explore** from the main menu.
1. Select a Tempo data source.
1. Select the **TraceQL** tab.
1. Start your query on the text line by entering `{`. For help with TraceQL syntax, refer to the [Construct a TraceQL query documentation](https://grafana.com/docs/tempo/latest/traceql/#construct-a-traceql-query).
1. Optional: Use the Time picker drop-down to change the time and range for the query (refer to the [documentation for instructions](https://grafana.com/docs/grafana/latest/dashboards/use-dashboards/#set-dashboard-time-range)).
1. Once you have finished your query, select **Run query**.
This video provides and example of creating a TraceQL query using the custom tag grouping.
Optional: Select **Copy query from Search** to transfer a builder query to the editor.
1. Optional: Use the **Time picker** drop-down list to change the time and range for the query (refer to the [documentation for instructions](https://grafana.com/docs/grafana/latest/dashboards/use-dashboards/#set-dashboard-time-range)).
1. Once you've finished your query, select **Run query**.
![Query editor showing span results](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-ed-example-v11-a.png)
This video provides an example of creating a TraceQL query using the custom tag grouping.
{{< youtube id="fraepWra00Y" >}}
@ -62,15 +73,15 @@ To query a particular trace by its trace ID:
1. Enter the trace ID into the query field. For example: `41928b92edf1cdbe0ba6594baee5ae9`
1. Click **Run query** or use the keyboard shortcut Shift + Enter.
![Search for a trace ID using the TraceQL query editor](/media/docs/tempo/traceql/screenshot-traceql-editor-traceID.png)
![Search for a trace ID using the TraceQL query editor](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-trace-id-v11.png)
## Use autocomplete to write queries
You can use the query editors autocomplete suggestions to write queries.
The editor detects span sets to provide relevant autocomplete options.
It uses regular expressions (regex) to detect where it's inside a spanset and provide attribute names, scopes, intrinsic names, logic operators, or attribute values from Tempo's API, depending on what's expected for the current situation.
The editor detects spansets to provide relevant autocomplete options.
It uses regular expressions (regex) to detect where it is inside a spanset and provide attribute names, scopes, intrinsic names, logic operators, or attribute values from the Tempo API, depending on what's expected for the current situation.
![Query editor showing the auto-complete feature](/media/docs/tempo/traceql/screenshot-traceql-query-editor-auto-complete-v10.png)
![Query editor showing the auto-complete feature](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-editor-autocomplete.png)
To create a query using autocomplete, follow these steps:
@ -84,17 +95,37 @@ To create a query using autocomplete, follow these steps:
## View query results
Query results for both the editor and the builder are returned in a table. Selecting the Trace ID or Span ID provides more detailed information.
Query results appear in a table, such as **Table - Traces**, under the query editor.
Each span (and the trace it belongs to) matching the query conditions is returned by the query.
If there are no filter conditions, all spans are matching and thus returned with their associated traces.
Selecting the trace ID from the returned results opens a trace diagram. Selecting a span from the returned results opens a trace diagram and reveals the relevant span in the trace diagram (the highlighted blue line).
A query is performed against a defined time interval, relative (for example, the last 3 hours) or absolute (for example, from X date-time to Y date-time).
The query response is also limited by the number of traces (**Limit**) and spans per spanset (**Span Limit**).
In the trace diagram, the bold text on the left side of each span indicates the service name, for example `mythical-requester: requester`, and it is hidden when subsequent spans have the same service name (nested spans).
Each service has a color assigned to it, which is visible to the left of the name and timeline in the graph.
Spans with the same color belong to the same service. The grey text to the right of the service name indicates the span name.
![TraceQL in Grafana](/media/docs/tempo/traceql/TraceQL-in-Grafana-v11.png)
![Query editor showing span results](/media/docs/tempo/traceql/screenshot-traceql-query-editor-results-v10.png)
1. TraceQL query editor
1. Query options: **Limit**, **Span Limit** and **Table Format** (Traces or Spans).
1. Trace (by Trace ID). The **Name** and **Service** columns are displaying the trace root span name and associated service.
1. Spans associated with the Trace.
### Streaming results
Selecting the trace ID from the returned results opens a trace diagram.
Selecting a span from the returned results opens a trace diagram and reveals the relevant span in the trace diagram.
For more information on span details, refer to [Traces in Explore](https://grafana.com/docs/grafana/latest/explore/trace-integration/#span-details).
![Selecting a trace ID or a span to view span details](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-span-details-v11.png)
### Focus on traces or spans
Under **Options**, you can choose to display the table as **Traces** or **Spans** focused.
When the **Table Type** option is set to **Spans**, the traces and spansets are flattened into a list of spans.
The trace service and trace name are added to the row of each span to add context.
Using the **Spans** option makes it easier access the spans to apply transformations and plot them in dashboards.
### Stream results
The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish.

View File

@ -17,45 +17,44 @@ labels:
# Write TraceQL queries using Search
The TraceQL query builder, located on the **Explore** > **Query type** > **Search** in Grafana, provides drop-downs and text fields to help you write a query.
The **Search** query builder, located on the **Explore** > **Query type** > **Search** in Grafana, provides drop-down lists and text fields to help you write a query.
The selections you make automatically generate a [TraceQL query](/docs/tempo/latest/traceql).
![The TraceQL query builder](/media/docs/tempo/traceql/screenshot-traceql-query-type-search-v10.png)
![The Search query builder](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-search-v11.png)
The builder lets you run the most common queries in as few clicks as possible. You don't need to know the underlying query language or database architecture to use it.
The builder supports a subset of TraceQL capabilities. For example, if you wish to use structural operators (`>>`, `>`, `~`), you need to use the query editor on the **TraceQL** tab.
You can use the query builders drop-downs to compose TraceQL queries. The selections you make automatically generate a [TraceQL query](/docs/tempo/latest/traceql).
The builder supports a subset of TraceQL capabilities, including some structural operators (`>>`, `>`, `=~`, `!=`).
To access **Search**, select your Tempo data source, and then choose **Explore** and select **Query type** > **Search**.
You can use the query builder to search trace data by resource service name, span name, duration, and one or more tags. The examples on this page use the default filters.
You can use the query builder to search trace data by resource service name, span name, duration, one or more tags. The examples on this page use the default filters.
In addition, you can add query builder blocks, view the query history, and use the **Inspector** to see details.
{{< figure src="/media/docs/tempo/traceql/screenshot-tempods-query-search.png" class="docs-image--no-shadow" max-width="750px" caption="Screenshot of the Tempo Search query type" >}}
{{< figure src="/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-builder-v11.png" class="docs-image--no-shadow" max-width="750px" caption="Screenshot of the Tempo Search query type" >}}
## Perform a search
To perform a search, you need to select filters and/or tags and then run the query. The results appear underneath the query builder.
To perform a search, you need to select filters and then run the query. The results appear underneath the query builder.
The screenshot identifies the areas used to perform a search.
{{< figure src="/media/docs/tempo/traceql/screenshot-tempods-query-search-parts.png" class="docs-image--no-shadow" max-width="750px" caption="Parts of Tempo Search query type" >}}
![Parts of the Search query builder](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-build-numbered-v11.png)
| Number | Name | Action | Comment |
| :----- | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Data source | Use the data source drop-down to select a Tempo data source. | Each data source has its own version of search. This Search is specific to the Tempo data source. |
| 2 | Query type tab | Select Search. | |
| 3 | Choose filter | Choose whether to filter using **Resource Service Name**, **Span Name**, and/or **Duration**. | Optional. You can execute an empty query in the Search tab. In TraceQL, `{}` is a valid query and is the default query until you choose options. |
| 4 | Filters conditions | Select options for one or more filters. For example, you can define a filter where **Resource Service Name** (`resource.service.name`) equals (`=`) `cloud-logs-archiver`. | Optional. At least one tag or filter must be defined. |
| 5 | Tags | Add tags for span, resource, or unscoped and define their conditions. | Optional. |
| :----- | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Data source | Use the data source drop-down list to select a Tempo data source. | Each data source has its own version of search. This **Search** is specific to the Tempo data source. |
| 2 | Query type | Select **Search**. | |
| 3 | Choose filter | Choose one or more of the filters. | Optional. You can execute an empty query in the Search tab. In TraceQL, `{}` is a valid query and is the default query to provide a list of all traces or spans. |
| 4 | Filters conditions | Select options for one or more filters. For example, you can define a filter where **Service Name** (`resource.service.name`) equals (`=`) `user`. | Optional. At least one tag or filter must be defined. |
| 5 | Tags and Aggregate by | Add tags for span, resource, or unscoped and define their conditions. Use **Aggregate by** to group results. | Optional. |
| 6 | TraceQL query | Displays the TraceQL query constructed by your selections. | This TraceQL query is executed when you select **Run query**. |
Every query searches the data for the selected time frame.
By default, queries run against data from the last hour.
Select Time range to the left of Run query to choose the time range for the data your query runs against.
Select **Time range** to the left of **Run query** to choose the time range for the data your query runs against.
Read the [dashboard time range](/docs/grafana/latest/dashboards/use-dashboards/#set-dashboard-time-range) documentation to learn more.
To access Search, use the following steps:
To access the **Search** query builder, use the following steps:
1. Sign into Grafana.
1. Select your Tempo data source.
@ -63,16 +62,20 @@ To access Search, use the following steps:
## Define filters
Using filters, you refine the data returned from the query by selecting **Resource Service Name**, **Span Name**, or **Duration**. The **Duration** represents span time, calculated by subtracting the end time from the start time of the span.
Using filters, you refine the data returned from the query by selecting **Service Name**, **Span Name**, **Status**, or **Duration**.
**Duration** represents span time, calculated by subtracting the end time from the start time of the span.
Grafana administrators can change the default filters using the Tempo data source configuration.
Filters can be limited by the operators. The available operators are determined by the field type.
For example, **Span Name** and **Resource Service Name** are string fields so the comparison operators are equals (`=`), not equal (`!=`), or regular expressions (`=~`).
Filters can be limited by the operators.
The field type determines the available operators.
For example, **Span Name** and **Service Name** are string fields so the comparison operators are equals (`=`), not equal (`!=`), matches regular expressions (`=~`), or doesn't match regular expression (`!~`).
**Duration** is a duration field type and uses range selections (`>`, `>=`, `<`, `<=`).
When you select multiple values for the same filter, Grafana automatically changes the operator to the regex operator `=~` and concatenates the values with a `|`. This capability only applies to fields with drop-down value selection.
When you select multiple values for the same filter, Grafana automatically changes the operator to the regular expression (regex) operator `=~` and concatenates the values with a `|`.
This capability only applies to fields with drop-down value selection.
For example, if you choose **Span Name** `= get` and then **Span Name** `= log_results_cache,` operator drop-down changes from `=` to `=~` and both `get` and `log_results_cache` are listed in the **Span Name** field. The resulting query is updated with this:
For example, if you choose **Span Name** `= get` and then **Span Name** `= log_results_cache,` operator drop-down list changes from `=` to `=~` and both `get` and `log_results_cache` are listed in the **Span Name** field.
The resulting query is updated with this:
`{duration>5ms && duration<10ms && name=~"get|log_results_cache"}`
@ -80,7 +83,7 @@ To define filters, follow these steps:
1. Choose one of the filters.
1. Select a comparison operator from the drop-down.
1. **Resource Service Name** and **Span Name** only: Select one or more values from the drop-down.
1. **Service Name**, **Span Name**, and **Status** only: Select one or more values from the drop-down.
1. **Duration** only: Enter values and units for the range and choose comparison operators for the drop-downs. Units can be nanoseconds (`ns`), milliseconds (`ms`), seconds (`s`), minutes (`m`), and hours (`h`).
You can either select **Run query** to execute the query or define tags and then run the query.
@ -97,9 +100,9 @@ To add a tag, follow these steps:
1. Select a tag from the **Select tag** drop-down.
1. Select a comparison operator.
1. Select a value from the **Select value** drop-down. This field is populated based upon the tag.
1. Optional: Select **+** to add an additional tag.
1. Optional: Select **+** to add another tag.
## Optional: Use Aggregate by
### Optional: Use Aggregate by
{{% admonition type="warning" %}}
**Aggregate by** is an [experimental feature](/docs/release-life-cycle/). Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided.
@ -127,7 +130,7 @@ The results are shown in the same order used in **Aggregate by**.
For example, **Aggregate by** lists `intrinsic.name` followed by `span.http.user_agent`.
The first column in the results Table shows **name** and then **span.http.user_agent**.
![Use Aggregate by to calculate RED metrics for spans and group by attributes](/media/docs/tempo/traceql/screenshot-traces-aggregate-by.png)
![Use Aggregate by to calculate RED metrics for spans and group by attributes](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-build-aggregate-v11-a.png)
To use this capability:
@ -141,22 +144,26 @@ To use this capability:
{{< youtube id="g97CjKOZqT4" >}}
### Optional: Add queries
### Optional: Add query and service graph blocks
Using **Add query**, you can have successive queries that run in sequential order.
Using **Add query**, you can have successive query or service node blocks that run in sequential order.
For example, query A runs and then query B.
You can reorder the queries by dragging and dropping them above or below other queries.
Select **+ Add query** to add another query block.
### Run queries and view results
For more information, refer to [Use query types together](/docs/grafana/<GRAFANA_VERSION>/datasources/tempo/query-editor/).
## Run queries and view results
Select **Run query** to run the TraceQL query (1 in the screenshot).
Queries can take a little while to return results. The results appear in a table underneath the query builder. Selecting a Trace ID (2 in the screenshot) displays more detailed information (3 in the screenshot).
Queries can take a little while to return results. The results appear in a table underneath the query builder.
Selecting a Trace ID (2 in the screenshot) displays more detailed information (3 in the screenshot).
**Span Filters** (4 in the screenshot) provide an additional to refine the query results.
{{< figure src="/media/docs/tempo/traceql/screenshot-tempods-query-results.png" class="docs-image--no-shadow" max-width="750px" caption="Tempo Search query type results" >}}
![Query results with numbered sections](/media/docs/grafana/data-sources/tempo/query-editor/tempo-ds-query-results-numbered-v11.png)
#### Streaming results
### Stream results
The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish.
@ -171,3 +178,16 @@ Streaming is available for both the **Search** and **TraceQL** query types.
You'll get immediate visibility of incoming traces on the results table.
{{< video-embed src="/media/docs/grafana/data-sources/tempo-streaming-v2.mp4" >}}
### Use filters and tags on spans
Using **Span Filters**, you can use filters to refine results when viewing span details.
These filters are available when viewing details for a trace.
You can continue to apply filters until you have narrowed down your resulting spans to the select few you are most interested in.
**Service Name**, **Span Name**, **Duration**, and **Tags** have the same function and operation as the filters of the same name in the **Search** query builder.
In addition, you can search for a keyword, opt to **Show matches only**, opt to **Show critical path only**, and browse matches using **Prev** and **Next**.
Use **Clear** to reset the filters.

View File

@ -22,7 +22,7 @@ weight: -44
Welcome to Grafana 11.2! We've made a number of improvements in this release, including a Grafana Cloud Migration Assistant in public preview, several new transformations, and a centralized page for viewing your alert history. We've also released several new data sources to help you visualize data from Zendesk, Catchpoint, and Yugabyte. Read on to learn more about these and all the new features in v11.2.
<!-- {{< youtube id="s6IYpILVDSM" >}} -->
{{< youtube id="s6IYpILVDSM" >}}
For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v11.2, check out our [Upgrade Guide](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/upgrade-guide/upgrade-v11.2/).
@ -38,6 +38,8 @@ This intuitive UI offers real-time updates on your migration status, making your
Ready to make the move? Explore our [migration guide](https://grafana.com/docs/grafana-cloud/account-management/migration-guide/) to learn more about the Cloud Migration Assistant today and begin your effortless transition to Grafana Cloud.
{{< youtube id="66W1UMHtX3U" >}}
## Navigation bookmarks
<!-- #grafana-frontend-platform -->
@ -110,7 +112,7 @@ Data links in canvas elements can also be configured to open with a single click
As part of this improvement, we've also added the ability to control the order in which data links are displayed by dragging and dropping them. This improvement has been added to all visualizations.
{{< video-embed src="/media/docs/grafana/panels-visualizations/canvas-oneclick-tooltips-v4-11.2.mp4" >}}
{{< youtube id="zOsM8VqwYpw" >}}
In future releases, we'll add one-click functionality to data links in other Grafana visualizations.
@ -132,6 +134,8 @@ Pagination is especially useful if you're running a query on a dynamic data sour
This feature is [a community contribution](https://github.com/grafana/grafana/pull/89586) ❤️
{{< youtube id="mgkjWJvYoHk" >}}
[Documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/state-timeline/#page-size-enable-pagination)
## Alerting
@ -150,7 +154,7 @@ For Grafana Enterprise and OSS users:
To try out the new alert history page, enable the `alertingCentralAlertHistory` feature toggle and [configure Loki annotations](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/configure-alert-state-history/).
{{< figure src="/media/docs/alerting/alert-state-history.png" alt="Alert state history page" >}}
{{< youtube id="0fNtby8ieEw" >}}
[Documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/manage-notifications/view-state-health/)

6
go.mod
View File

@ -74,9 +74,9 @@ require (
github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20240827075410-70248a7a3a67 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team
github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team
github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics

12
go.sum
View File

@ -2246,12 +2246,12 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20240827075410-70248a7a3a67 h1:3spByRvTR3Qo7uDCEVVLB7+5VYH1q4hxwqVLdNpcS6k=
github.com/grafana/alerting v0.0.0-20240827075410-70248a7a3a67/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4=
github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db h1:z++X4DdoX+aNlZNT1ZY4cykiFay4+f077pa0AG48SGg=
github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db/go.mod h1:ptt910z9KFfpVSIbSbXvTRR7tS19mxD7EtmVbbJi/WE=
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso=
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41 h1:p+UsX43BoDH5YlG6xUd9xDS3M4sWouy8VJ+ODv5S6uE=
github.com/grafana/alerting v0.0.0-20240822131459-9daa6239cc41/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4=
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ=
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=

View File

@ -714,6 +714,8 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grafana/authlib v0.0.0-20240730122259-a0d13672efb1/go.mod h1:YA9We4kTafu7mlMnUh3In6Q2wpg8fYN3ycgCKOK1TB8=
github.com/grafana/authlib/claims v0.0.0-20240809101159-74eaccc31a06/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/authlib/claims v0.0.0-20240827173836-433b4fdb2f25/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/authlib/claims v0.0.0-20240827201239-81213c6670d3/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU=
github.com/grafana/grafana-plugin-sdk-go v0.235.0/go.mod h1:6n9LbrjGL3xAATntYVNcIi90G9BVHRJjzHKz5FXVfWw=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b h1:HCbWyVL6vi7gxyO76gQksSPH203oBJ1MJ3JcG1OQlsg=

View File

@ -122,7 +122,7 @@
"@types/lodash": "4.17.7",
"@types/logfmt": "^1.2.3",
"@types/lucene": "^2",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/node-forge": "^1",
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4",
"@types/pluralize": "^0.0.33",
@ -221,7 +221,7 @@
"react-test-renderer": "18.2.0",
"redux-mock-store": "1.5.4",
"rimraf": "5.0.7",
"rudder-sdk-js": "2.48.16",
"rudder-sdk-js": "2.48.17",
"sass": "1.77.8",
"sass-loader": "14.2.1",
"smtp-tester": "^2.1.0",
@ -231,10 +231,10 @@
"terser-webpack-plugin": "5.3.10",
"testing-library-selector": "0.3.1",
"tracelib": "1.0.1",
"ts-jest": "29.2.4",
"ts-jest": "29.2.5",
"ts-node": "10.9.2",
"typescript": "5.5.4",
"webpack": "5.91.0",
"webpack": "5.94.0",
"webpack-assets-manifest": "^5.1.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "5.1.4",
@ -249,7 +249,7 @@
"@emotion/css": "11.11.2",
"@emotion/react": "11.11.4",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@floating-ui/react": "0.26.22",
"@floating-ui/react": "0.26.23",
"@formatjs/intl-durationformat": "^0.2.4",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/aws-sdk": "0.4.1",
@ -285,7 +285,7 @@
"@msagl/parser": "^1.1.19",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/exporter-collector": "0.25.0",
"@opentelemetry/semantic-conventions": "1.25.1",
"@opentelemetry/semantic-conventions": "1.27.0",
"@popperjs/core": "2.11.8",
"@react-aria/dialog": "3.5.17",
"@react-aria/focus": "3.18.2",
@ -440,7 +440,7 @@
"engines": {
"node": ">= 20"
},
"packageManager": "yarn@4.4.0",
"packageManager": "yarn@4.4.1",
"dependenciesMeta": {
"prettier@3.3.3": {
"unplugged": true

View File

@ -66,7 +66,7 @@
"@types/dompurify": "^3.0.0",
"@types/history": "4.7.11",
"@types/lodash": "4.17.7",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/papaparse": "5.3.14",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",

View File

@ -19,7 +19,14 @@ describe('sanitizeUrl', () => {
});
});
// write test to sanitize xss payloads using the sanitize function
describe('sanitizeIframe', () => {
it('should sanitize iframe tags', () => {
const html = '<iframe src="javascript:alert(document.domain)"></iframe>';
const str = sanitizeTextPanelContent(html);
expect(str).toBe('<iframe src="about:blank" sandbox credentialless referrerpolicy=no-referrer></iframe>');
});
});
describe('sanitize', () => {
it('should sanitize xss payload', () => {
const html = '<script>alert(1)</script>';

View File

@ -7,7 +7,20 @@ const XSSWL = Object.keys(xss.whiteList).reduce<xss.IWhiteList>((acc, element) =
return acc;
}, {});
// Add iframe tags to XSSWL.
// We don't allow the sandbox attribute, since it can be overridden, instead we add it below.
XSSWL.iframe = ['src', 'width', 'height'];
const sanitizeTextPanelWhitelist = new xss.FilterXSS({
// Add sandbox attribute to iframe tags if an attribute is allowed.
onTagAttr: function (tag, name, value, isWhiteAttr) {
if (tag === 'iframe') {
return isWhiteAttr
? ` ${name}="${xss.escapeAttrValue(sanitizeUrl(value))}" sandbox credentialless referrerpolicy=no-referrer`
: '';
}
return;
},
whiteList: XSSWL,
css: {
whiteList: {

View File

@ -45,6 +45,7 @@ export interface FeatureToggles {
cloudWatchCrossAccountQuerying?: boolean;
showDashboardValidationWarnings?: boolean;
mysqlAnsiQuotes?: boolean;
mysqlParseTime?: boolean;
accessControlOnCall?: boolean;
nestedFolders?: boolean;
alertingBacktesting?: boolean;

View File

@ -40,7 +40,7 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "15.2.3",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"esbuild": "0.20.2",
"rimraf": "5.0.7",
"rollup": "2.79.1",

View File

@ -68,7 +68,7 @@
"@types/d3": "^7",
"@types/jest": "^29.5.4",
"@types/lodash": "4.17.7",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/react": "18.3.3",
"@types/react-virtualized-auto-sizer": "1.0.4",
"@types/tinycolor2": "1.4.6",
@ -80,7 +80,7 @@
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-esbuild": "5.0.0",
"rollup-plugin-node-externals": "^5.0.0",
"ts-jest": "29.2.4",
"ts-jest": "29.2.5",
"ts-node": "10.9.2",
"typescript": "5.5.4"
},

View File

@ -45,7 +45,7 @@
"@svgr/plugin-prettier": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"@types/babel__core": "^7",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"esbuild": "0.20.2",

View File

@ -36,13 +36,13 @@
"@testing-library/react": "15.0.2",
"@testing-library/user-event": "14.5.2",
"@types/jest": "^29.5.4",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/react": "18.3.3",
"@types/systemjs": "6.13.5",
"@types/testing-library__jest-dom": "5.14.9",
"jest": "^29.6.4",
"react": "18.2.0",
"ts-jest": "29.2.4",
"ts-jest": "29.2.5",
"ts-node": "10.9.2",
"typescript": "5.5.4"
},

View File

@ -18,7 +18,7 @@
"replace-in-file-webpack-plugin": "1.0.6",
"swc-loader": "0.2.6",
"typescript": "5.5.4",
"webpack": "5.91.0"
"webpack": "5.94.0"
},
"packageManager": "yarn@4.4.0"
"packageManager": "yarn@4.4.1"
}

View File

@ -37,10 +37,10 @@
},
"dependencies": {
"@emotion/css": "11.11.2",
"@floating-ui/react": "0.26.22",
"@floating-ui/react": "0.26.23",
"@grafana/data": "11.3.0-pre",
"@grafana/experimental": "1.8.0",
"@grafana/faro-web-sdk": "1.9.0",
"@grafana/faro-web-sdk": "1.9.1",
"@grafana/runtime": "11.3.0-pre",
"@grafana/schema": "11.3.0-pre",
"@grafana/ui": "11.3.0-pre",
@ -49,7 +49,7 @@
"@lezer/common": "1.2.1",
"@lezer/highlight": "1.2.1",
"@lezer/lr": "1.4.2",
"@prometheus-io/lezer-promql": "0.53.2",
"@prometheus-io/lezer-promql": "0.54.1",
"@reduxjs/toolkit": "2.2.7",
"d3": "7.9.0",
"date-fns": "3.6.0",
@ -92,7 +92,7 @@
"@types/jest": "29.5.12",
"@types/jquery": "3.5.30",
"@types/lodash": "4.17.7",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/pluralize": "^0.0.33",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
@ -137,7 +137,7 @@
"testing-library-selector": "0.3.1",
"ts-node": "10.9.2",
"typescript": "5.5.4",
"webpack": "5.91.0",
"webpack": "5.94.0",
"webpack-cli": "5.1.4"
},
"peerDependencies": {

View File

@ -658,6 +658,7 @@ export enum BarGaugeValueMode {
*/
export enum BarGaugeNamePlacement {
Auto = 'auto',
Hidden = 'hidden',
Left = 'left',
Top = 'top',
}

View File

@ -251,7 +251,7 @@ BarGaugeDisplayMode: "basic" | "lcd" | "gradient" @cuetsy(kind="enum")
BarGaugeValueMode: "color" | "text" | "hidden" @cuetsy(kind="enum")
// Allows for the bar gauge name to be placed explicitly
BarGaugeNamePlacement: "auto" | "top" | "left" @cuetsy(kind="enum")
BarGaugeNamePlacement: "auto" | "top" | "left" | "hidden" @cuetsy(kind="enum")
// Allows for the bar gauge size to be set explicitly
BarGaugeSizing: "auto" | "manual" @cuetsy(kind="enum")

View File

@ -42,7 +42,7 @@
"@testing-library/user-event": "14.5.2",
"@types/jest": "^29.5.4",
"@types/lodash": "4.17.7",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/react": "18.3.3",
"@types/react-dom": "18.2.25",
"@types/react-virtualized-auto-sizer": "1.0.4",
@ -50,7 +50,7 @@
"@types/testing-library__jest-dom": "5.14.9",
"@types/uuid": "9.0.8",
"jest": "^29.6.4",
"ts-jest": "29.2.4",
"ts-jest": "29.2.5",
"ts-node": "10.9.2",
"typescript": "5.5.4"
},

View File

@ -49,7 +49,7 @@
"dependencies": {
"@emotion/css": "11.11.2",
"@emotion/react": "11.11.4",
"@floating-ui/react": "0.26.22",
"@floating-ui/react": "0.26.23",
"@grafana/data": "11.3.0-pre",
"@grafana/e2e-selectors": "11.3.0-pre",
"@grafana/faro-web-sdk": "^1.3.6",
@ -145,7 +145,7 @@
"@types/is-hotkey": "0.1.10",
"@types/jest": "29.5.12",
"@types/mock-raf": "1.0.6",
"@types/node": "20.14.14",
"@types/node": "20.16.2",
"@types/prismjs": "1.26.4",
"@types/react": "18.3.3",
"@types/react-color": "3.0.12",
@ -186,7 +186,7 @@
"storybook-dark-mode": "^4.0.1",
"style-loader": "4.0.0",
"typescript": "5.5.4",
"webpack": "5.91.0"
"webpack": "5.94.0"
},
"peerDependencies": {
"react": "^18.0.0",

View File

@ -229,7 +229,7 @@ interface CellColors {
interface TitleDimensions {
fontSize: number;
placement: 'above' | 'left' | 'below';
placement: 'above' | 'left' | 'below' | 'hidden';
width: number;
height: number;
}
@ -246,6 +246,15 @@ function calculateTitleDimensions(props: Props): TitleDimensions {
return { fontSize: 0, width: 0, height: 0, placement: 'above' };
}
if (namePlacement === BarGaugeNamePlacement.Hidden) {
return {
fontSize: 0,
width: 0,
height: 0,
placement: BarGaugeNamePlacement.Hidden,
};
}
if (isVertical(orientation)) {
const fontSize = text?.titleSize ?? 14;
return {
@ -316,6 +325,9 @@ export function getTitleStyles(props: Props): { wrapper: CSSProperties; title: C
alignSelf: 'center',
};
if (titleDim.placement === 'hidden') {
titleStyles.display = 'none';
} else {
if (isVertical(props.orientation)) {
wrapperStyles.flexDirection = 'column-reverse';
titleStyles.textAlign = 'center';
@ -330,6 +342,7 @@ export function getTitleStyles(props: Props): { wrapper: CSSProperties; title: C
titleStyles.paddingRight = '10px';
}
}
}
return {
wrapper: wrapperStyles,

View File

@ -3,6 +3,8 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react';
import { Chance } from 'chance';
import { ComponentProps, useEffect, useState } from 'react';
import { Field } from '../Forms/Field';
import { Combobox, Option, Value } from './Combobox';
const chance = new Chance();
@ -15,11 +17,18 @@ const meta: Meta<PropsAndCustomArgs> = {
args: {
loading: undefined,
invalid: undefined,
width: 30,
placeholder: 'Select an option...',
options: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Carrot', value: 'carrot' },
// Long label to test overflow
{
label:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
value: 'long-text',
},
{ label: 'Dill', value: 'dill' },
{ label: 'Eggplant', value: 'eggplant' },
{ label: 'Fennel', value: 'fennel' },
@ -40,7 +49,9 @@ const meta: Meta<PropsAndCustomArgs> = {
const BasicWithState: StoryFn<typeof Combobox> = (args) => {
const [value, setValue] = useState(args.value);
return (
<Field label="Test input" description="Input with a few options">
<Combobox
id="test-combobox"
{...args}
value={value}
onChange={(val) => {
@ -48,6 +59,7 @@ const BasicWithState: StoryFn<typeof Combobox> = (args) => {
action('onChange')(val);
}}
/>
</Field>
);
};
@ -56,10 +68,10 @@ type Story = StoryObj<typeof Combobox>;
export const Basic: Story = {};
async function generateOptions(amount: number): Promise<Option[]> {
return Array.from({ length: amount }, () => ({
label: chance.name(),
return Array.from({ length: amount }, (_, index) => ({
label: chance.sentence({ words: index % 5 }),
value: chance.guid(),
description: chance.sentence(),
//description: chance.sentence(),
}));
}

View File

@ -1,8 +1,8 @@
import { cx } from '@emotion/css';
import { autoUpdate, flip, useFloating } from '@floating-ui/react';
import { autoUpdate, flip, size, useFloating } from '@floating-ui/react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCombobox } from 'downshift';
import { useCallback, useMemo, useRef, useState } from 'react';
import { SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useStyles2 } from '../../themes';
import { t } from '../../utils/i18n';
@ -19,7 +19,7 @@ export type Option = {
};
interface ComboboxProps
extends Omit<InputProps, 'width' | 'prefix' | 'suffix' | 'value' | 'addonBefore' | 'addonAfter' | 'onChange'> {
extends Omit<InputProps, 'prefix' | 'suffix' | 'value' | 'addonBefore' | 'addonAfter' | 'onChange'> {
onChange: (val: Option | null) => void;
value: Value | null;
options: Option[];
@ -42,11 +42,16 @@ function itemFilter(inputValue: string) {
}
function estimateSize() {
return 60;
return 45;
}
export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxProps) => {
const MIN_WIDTH = 400;
const MIN_HEIGHT = 400;
// On every 100th index we will recalculate the width of the popover.
const INDEX_WIDTH_CALCULATION = 100;
// A multiplier guesstimate times the amount of characters. If any padding or image support etc. is added this will need to be updated.
const WIDTH_MULTIPLIER = 7.3;
export const Combobox = ({ options, onChange, value, id, ...restProps }: ComboboxProps) => {
const [items, setItems] = useState(options);
const selectedItemIndex = useMemo(
() => options.findIndex((option) => option.value === value) || null,
@ -55,18 +60,33 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
const selectedItem = selectedItemIndex ? options[selectedItemIndex] : null;
const inputRef = useRef<HTMLInputElement>(null);
const floatingRef = useRef(null);
const styles = useStyles2(getComboboxStyles);
const floatingRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
const styles = useStyles2(getComboboxStyles);
const [popoverMaxWidth, setPopoverMaxWidth] = useState<number | undefined>(undefined);
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
const virtualizerOptions = {
count: items.length,
getScrollElement: () => floatingRef.current,
estimateSize,
overscan: 2,
});
overscan: 4,
};
const { getInputProps, getMenuProps, getItemProps, isOpen, highlightedIndex, setInputValue, selectItem } =
useCombobox({
const rowVirtualizer = useVirtualizer(virtualizerOptions);
const {
getInputProps,
getMenuProps,
getItemProps,
isOpen,
highlightedIndex,
setInputValue,
selectItem,
openMenu,
closeMenu,
} = useCombobox({
inputId: id,
items,
itemToString,
selectedItem,
@ -100,21 +120,27 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
const middleware = [
flip({
// see https://floating-ui.com/docs/flip#combining-with-shift
crossAxis: false,
crossAxis: true,
boundary: document.body,
fallbackPlacements: ['top'],
}),
size({
apply({ availableWidth }) {
setPopoverMaxWidth(availableWidth);
},
}),
];
const elements = { reference: inputRef.current, floating: floatingRef.current };
const { floatingStyles } = useFloating({
open: isOpen,
placement: 'bottom',
placement: 'bottom-start',
middleware,
elements,
whileElementsMounted: autoUpdate,
});
const hasMinHeight = isOpen && rowVirtualizer.getTotalSize() >= MIN_WIDTH;
const hasMinHeight = isOpen && rowVirtualizer.getTotalSize() >= MIN_HEIGHT;
useDynamicWidth(items, rowVirtualizer.range, setPopoverWidth);
return (
<div>
@ -127,6 +153,7 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
className={styles.clear}
title={t('combobox.clear.title', 'Clear value')}
tabIndex={0}
role="button"
onClick={() => {
selectItem(null);
}}
@ -137,7 +164,16 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
}}
/>
)}
<Icon name={isOpen ? 'search' : 'angle-down'} />
<Icon
name={isOpen ? 'search' : 'angle-down'}
onClick={() => {
if (isOpen) {
closeMenu();
} else {
openMenu();
}
}}
/>
</>
}
{...restProps}
@ -153,7 +189,12 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
/>
<div
className={cx(styles.menu, hasMinHeight && styles.menuHeight)}
style={{ ...floatingStyles, width: elements.reference?.getBoundingClientRect().width }}
style={{
...floatingStyles,
maxWidth: popoverMaxWidth,
minWidth: inputRef.current?.offsetWidth,
width: popoverWidth,
}}
{...getMenuProps({ ref: floatingRef })}
>
{isOpen && (
@ -162,20 +203,20 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
return (
<li
key={items[virtualRow.index].value}
{...getItemProps({ item: items[virtualRow.index], index: virtualRow.index })}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
className={cx(
styles.option,
selectedItem && items[virtualRow.index].value === selectedItem.value && styles.optionSelected,
highlightedIndex === virtualRow.index && styles.optionFocused
)}
style={{
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
{...getItemProps({ item: items[virtualRow.index], index: virtualRow.index })}
>
<div className={styles.optionBody}>
<span>{items[virtualRow.index].label}</span>
<span className={styles.optionLabel}>{items[virtualRow.index].label}</span>
{items[virtualRow.index].description && (
<span className={styles.optionDescription}>{items[virtualRow.index].description}</span>
)}
@ -189,3 +230,46 @@ export const Combobox = ({ options, onChange, value, ...restProps }: ComboboxPro
</div>
);
};
const useDynamicWidth = (
items: Option[],
range: { startIndex: number; endIndex: number } | null,
setPopoverWidth: { (value: SetStateAction<number | undefined>): void }
) => {
useEffect(() => {
if (range === null) {
return;
}
const startVisibleIndex = range?.startIndex;
const endVisibleIndex = range?.endIndex;
if (typeof startVisibleIndex === 'undefined' || typeof endVisibleIndex === 'undefined') {
return;
}
// Scroll down and default case
if (
startVisibleIndex === 0 ||
(startVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && startVisibleIndex >= INDEX_WIDTH_CALCULATION)
) {
let maxLength = 0;
const calculationEnd = Math.min(items.length, endVisibleIndex + INDEX_WIDTH_CALCULATION);
for (let i = startVisibleIndex; i < calculationEnd; i++) {
maxLength = Math.max(maxLength, items[i].label.length);
}
setPopoverWidth(maxLength * WIDTH_MULTIPLIER);
} else if (endVisibleIndex % INDEX_WIDTH_CALCULATION === 0 && endVisibleIndex >= INDEX_WIDTH_CALCULATION) {
// Scroll up case
let maxLength = 0;
const calculationStart = Math.max(0, startVisibleIndex - INDEX_WIDTH_CALCULATION);
for (let i = calculationStart; i < endVisibleIndex; i++) {
maxLength = Math.max(maxLength, items[i].label.length);
}
setPopoverWidth(maxLength * WIDTH_MULTIPLIER);
}
}, [items, range, setPopoverWidth]);
};

View File

@ -22,16 +22,16 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
}),
option: css({
label: 'grafana-select-option',
padding: '8px',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
flexShrink: 0,
whiteSpace: 'nowrap',
width: '100%',
overflow: 'hidden',
cursor: 'pointer',
borderLeft: '2px solid transparent',
padding: theme.spacing.x1,
boxSizing: 'border-box',
height: 'auto',
'&:hover': {
background: theme.colors.action.hover,
'@media (forced-colors: active), (prefers-contrast: more)': {
@ -45,14 +45,21 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
fontWeight: theme.typography.fontWeightMedium,
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
}),
optionLabel: css({
label: 'grafana-select-option-label',
textOverflow: 'ellipsis',
overflow: 'hidden',
}),
optionDescription: css({
label: 'grafana-select-option-description',
fontWeight: 'normal',
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,
whiteSpace: 'normal',
lineHeight: theme.typography.body.lineHeight,
textOverflow: 'ellipsis',
overflow: 'hidden',
}),
optionFocused: css({
label: 'grafana-select-option-focused',
@ -71,7 +78,6 @@ export const getComboboxStyles = (theme: GrafanaTheme2) => {
display: 'block',
height: '100%',
position: 'absolute',
transform: 'translateX(-50%)',
width: theme.spacing(0.5),
left: 0,
top: 0,

View File

@ -15,6 +15,7 @@ export interface WithContextMenuProps {
export const WithContextMenu = ({ children, renderMenuItems, focusOnOpen = true }: WithContextMenuProps) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const isBodyScrolling = window.grafanaBootData?.settings.featureToggles.bodyScrolling;
return (
<>
{children({
@ -22,7 +23,7 @@ export const WithContextMenu = ({ children, renderMenuItems, focusOnOpen = true
setIsMenuOpen(true);
setMenuPosition({
x: e.pageX,
y: e.pageY,
y: isBodyScrolling ? e.pageY - window.scrollY : e.pageY,
});
},
})}

View File

@ -137,5 +137,8 @@ export function getUtilityClassStyles(theme: GrafanaTheme2) {
boxShadow: 'none',
},
},
'.typeahead': {
zIndex: theme.zIndex.typeahead,
},
});
}

View File

@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/services/apikey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/web"
@ -114,59 +113,15 @@ func (hs *HTTPServer) DeleteAPIKey(c *contextmodel.ReqContext) response.Response
// see: https://grafana.com/docs/grafana/next/administration/api-keys/#migrate-api-keys-to-grafana-service-accounts-using-the-api.
//
// Responses:
// 200: postAPIkeyResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 409: conflictError
// 500: internalServerError
// 301: statusMovedPermanently
func (hs *HTTPServer) AddAPIKey(c *contextmodel.ReqContext) response.Response {
cmd := apikey.AddCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if !cmd.Role.IsValid() {
return response.Error(http.StatusBadRequest, "Invalid role specified", nil)
}
if !c.SignedInUser.GetOrgRole().Includes(cmd.Role) {
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
}
// Set the Location header to the new URL
hs.log.Warn("Obsolete and Permanently moved API endpoint called", "path", c.Req.URL.Path)
c.Context.Resp.Header().Set("Location", "/api/serviceaccounts/tokens")
if hs.Cfg.ApiKeyMaxSecondsToLive != -1 {
if cmd.SecondsToLive == 0 {
return response.Error(http.StatusBadRequest, "Number of seconds before expiration should be set", nil)
}
if cmd.SecondsToLive > hs.Cfg.ApiKeyMaxSecondsToLive {
return response.Error(http.StatusBadRequest, "Number of seconds before expiration is greater than the global limit", nil)
}
}
cmd.OrgID = c.SignedInUser.GetOrgID()
newKeyInfo, err := apikeygen.New(cmd.OrgID, cmd.Name)
if err != nil {
return response.Error(http.StatusInternalServerError, "Generating API key failed", err)
}
cmd.Key = newKeyInfo.HashedKey
key, err := hs.apiKeyService.AddAPIKey(c.Req.Context(), &cmd)
if err != nil {
if errors.Is(err, apikey.ErrInvalidExpiration) {
return response.Error(http.StatusBadRequest, err.Error(), nil)
}
if errors.Is(err, apikey.ErrDuplicate) {
return response.Error(http.StatusConflict, err.Error(), nil)
}
return response.Error(http.StatusInternalServerError, "Failed to add API Key", err)
}
result := &dtos.NewApiKeyResult{
ID: key.ID,
Name: key.Name,
Key: newKeyInfo.ClientSecret,
}
return response.JSON(http.StatusOK, result)
// Respond with a 301 Moved Permanently status code
// the Location header is enough for clients to know where to go next.
return response.JSON(http.StatusMovedPermanently, nil)
}
// swagger:parameters getAPIkeys
@ -178,13 +133,6 @@ type GetAPIkeysParams struct {
IncludeExpired bool `json:"includeExpired"`
}
// swagger:parameters addAPIkey
type AddAPIkeyParams struct {
// in:body
// required:true
Body apikey.AddCommand
}
// swagger:parameters deleteAPIkey
type DeleteAPIkeyParams struct {
// in:path

View File

@ -370,7 +370,7 @@ func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.R
return response.JSON(http.StatusOK, util.DynMap{
"title": dash.Title,
"message": fmt.Sprintf("Dashboard %s moved to trash", dash.Title),
"message": fmt.Sprintf("Dashboard %s moved to Recently deleted", dash.Title),
"uid": dash.UID,
})
}

View File

@ -82,6 +82,11 @@ type UnauthorizedError GenericError
// swagger:response acceptedResponse
type AcceptedResponse GenericError
// StatusMovedPermanently
//
// swagger:response statusMovedPermanently
type StatusMovedPermanentlyRedirect GenericError
// documentation for PublicError defined in errutil.Error
// swagger:response publicErrorResponse

View File

@ -3,8 +3,8 @@ module github.com/grafana/grafana/pkg/apimachinery
go 1.23.0
require (
github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db // @grafana/identity-access-team
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 // @grafana/identity-access-team
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team
github.com/stretchr/testify v1.9.0
k8s.io/apimachinery v0.31.0
k8s.io/apiserver v0.31.0

View File

@ -28,10 +28,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db h1:z++X4DdoX+aNlZNT1ZY4cykiFay4+f077pa0AG48SGg=
github.com/grafana/authlib v0.0.0-20240814074258-eae7d47f01db/go.mod h1:ptt910z9KFfpVSIbSbXvTRR7tS19mxD7EtmVbbJi/WE=
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso=
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935 h1:nT4UY61s2flsiLkU2jDqtqFhOLwqh355+8ZhnavKoMQ=
github.com/grafana/authlib v0.0.0-20240827201526-24af227df935/go.mod h1:ER7bMzNNWTN/5Zl3pwqfgS6XEhcanjrvL7lOp8Ow6oc=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
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=

View File

@ -13,6 +13,10 @@ type IDClaimsWrapper struct {
Source Requester
}
func (i *IDClaimsWrapper) IsNil() bool {
return i.Source.IsNil()
}
// GetAuthenticatedBy implements claims.IdentityClaims.
func (i *IDClaimsWrapper) AuthenticatedBy() string {
return i.Source.GetAuthenticatedBy()

View File

@ -92,6 +92,10 @@ type GrafanaMetaAccessor interface {
GetOriginTimestamp() (*time.Time, error)
GetSpec() (any, error)
SetSpec(any) error
GetStatus() (any, error)
SetStatus(any) error
// Find a title in the object
// This will reflect the object and try to get:
@ -504,6 +508,36 @@ func (m *grafanaMetaAccessor) GetSpec() (spec any, err error) {
return
}
func (m *grafanaMetaAccessor) SetSpec(s any) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error setting spec")
}
}()
m.r.FieldByName("Spec").Set(reflect.ValueOf(s))
return
}
func (m *grafanaMetaAccessor) GetStatus() (status any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error reading status")
}
}()
status = m.r.FieldByName("Status").Interface()
return
}
func (m *grafanaMetaAccessor) SetStatus(s any) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error setting status")
}
}()
m.r.FieldByName("Status").Set(reflect.ValueOf(s))
return
}
func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string {
// look for Spec.Title or Spec.Name
spec := m.r.FieldByName("Spec")

View File

@ -170,6 +170,7 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) {
&SSOSettingList{},
&TeamBinding{},
&TeamBindingList{},
&TeamMemberList{},
)
}

View File

@ -23,11 +23,19 @@ type IdentityDisplayResults struct {
}
type IdentityDisplay struct {
IdentityType claims.IdentityType `json:"type"` // The namespaced UID, eg `user|api-key|...`
UID string `json:"uid"` // The namespaced UID, eg `xyz`
// Type of identity e.g. "user".
// For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24
IdentityType claims.IdentityType `json:"type"`
// UID for identity, is a unique value for the type within a namespace.
UID string `json:"uid"`
// Display name for identity.
Display string `json:"display"`
// AvatarURL is the url where we can get the avatar for identity
AvatarURL string `json:"avatarURL,omitempty"`
// Legacy internal ID -- usage of this value should be phased out
// InternalID is the legacy numreric id for identity, this is deprecated and should be phased out
InternalID int64 `json:"internalId,omitempty"`
}

View File

@ -50,8 +50,7 @@ type TeamSubject struct {
// Name is the unique identifier for subject.
Name string `json:"name,omitempty"`
// Permission subject has in permission.
// Can be either admin or member.
// Permission subject has in team.
Permission TeamPermission `json:"permission,omitempty"`
}
@ -60,6 +59,23 @@ type TeamRef struct {
Name string `json:"name,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type TeamMemberList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TeamMember `json:"items,omitempty"`
}
type TeamMember struct {
IdentityDisplay `json:",inline"`
// External is set if member ship was synced from external IDP.
External bool `json:"external,omitempty"`
// Permission member has in team.
Permission TeamPermission `json:"permission,omitempty"`
}
// TeamPermission for subject
// +enum
type TeamPermission string

View File

@ -362,6 +362,54 @@ func (in *TeamList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamMember) DeepCopyInto(out *TeamMember) {
*out = *in
out.IdentityDisplay = in.IdentityDisplay
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamMember.
func (in *TeamMember) DeepCopy() *TeamMember {
if in == nil {
return nil
}
out := new(TeamMember)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamMemberList) DeepCopyInto(out *TeamMemberList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]TeamMember, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TeamMemberList.
func (in *TeamMemberList) DeepCopy() *TeamMemberList {
if in == nil {
return nil
}
out := new(TeamMemberList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *TeamMemberList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TeamRef) DeepCopyInto(out *TeamRef) {
*out = *in

View File

@ -27,6 +27,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingList": schema_pkg_apis_identity_v0alpha1_TeamBindingList(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamBindingSpec": schema_pkg_apis_identity_v0alpha1_TeamBindingSpec(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamList": schema_pkg_apis_identity_v0alpha1_TeamList(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamMember": schema_pkg_apis_identity_v0alpha1_TeamMember(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamMemberList": schema_pkg_apis_identity_v0alpha1_TeamMemberList(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamRef": schema_pkg_apis_identity_v0alpha1_TeamRef(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSpec": schema_pkg_apis_identity_v0alpha1_TeamSpec(ref),
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamSubject": schema_pkg_apis_identity_v0alpha1_TeamSubject(ref),
@ -44,6 +46,7 @@ func schema_pkg_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallb
Properties: map[string]spec.Schema{
"type": {
SchemaProps: spec.SchemaProps{
Description: "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24",
Default: "",
Type: []string{"string"},
Format: "",
@ -51,7 +54,7 @@ func schema_pkg_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallb
},
"uid": {
SchemaProps: spec.SchemaProps{
Description: "The namespaced UID, eg `user|api-key|...`",
Description: "UID for identity, is a unique value for the type within a namespace.",
Default: "",
Type: []string{"string"},
Format: "",
@ -59,7 +62,7 @@ func schema_pkg_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallb
},
"display": {
SchemaProps: spec.SchemaProps{
Description: "The namespaced UID, eg `xyz`",
Description: "Display name for identity.",
Default: "",
Type: []string{"string"},
Format: "",
@ -67,13 +70,14 @@ func schema_pkg_apis_identity_v0alpha1_IdentityDisplay(ref common.ReferenceCallb
},
"avatarURL": {
SchemaProps: spec.SchemaProps{
Description: "AvatarURL is the url where we can get the avatar for identity",
Type: []string{"string"},
Format: "",
},
},
"internalId": {
SchemaProps: spec.SchemaProps{
Description: "Legacy internal ID -- usage of this value should be phased out",
Description: "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out",
Type: []string{"integer"},
Format: "int64",
},
@ -621,6 +625,119 @@ func schema_pkg_apis_identity_v0alpha1_TeamList(ref common.ReferenceCallback) co
}
}
func schema_pkg_apis_identity_v0alpha1_TeamMember(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"type": {
SchemaProps: spec.SchemaProps{
Description: "Type of identity e.g. \"user\". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"uid": {
SchemaProps: spec.SchemaProps{
Description: "UID for identity, is a unique value for the type within a namespace.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"display": {
SchemaProps: spec.SchemaProps{
Description: "Display name for identity.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"avatarURL": {
SchemaProps: spec.SchemaProps{
Description: "AvatarURL is the url where we can get the avatar for identity",
Type: []string{"string"},
Format: "",
},
},
"internalId": {
SchemaProps: spec.SchemaProps{
Description: "InternalID is the legacy numreric id for identity, this is deprecated and should be phased out",
Type: []string{"integer"},
Format: "int64",
},
},
"external": {
SchemaProps: spec.SchemaProps{
Description: "External is set if member ship was synced from external IDP.",
Type: []string{"boolean"},
Format: "",
},
},
"permission": {
SchemaProps: spec.SchemaProps{
Description: "Permission member has in team.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"admin", "member"},
},
},
},
Required: []string{"type", "uid", "display"},
},
},
}
}
func schema_pkg_apis_identity_v0alpha1_TeamMemberList(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/pkg/apis/identity/v0alpha1.TeamMember"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/identity/v0alpha1.TeamMember", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_identity_v0alpha1_TeamRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@ -679,7 +796,7 @@ func schema_pkg_apis_identity_v0alpha1_TeamSubject(ref common.ReferenceCallback)
},
"permission": {
SchemaProps: spec.SchemaProps{
Description: "Permission subject has in permission. Can be either admin or member.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`",
Description: "Permission subject has in team.\n\nPossible enum values:\n - `\"admin\"`\n - `\"member\"`",
Type: []string{"string"},
Format: "",
Enum: []interface{}{"admin", "member"},

View File

@ -4,7 +4,7 @@ go 1.23.0
require (
github.com/google/go-cmp v0.6.0
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1
github.com/prometheus/client_golang v1.20.0
github.com/stretchr/testify v1.9.0
@ -14,18 +14,22 @@ require (
k8s.io/component-base v0.31.0
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.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/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
@ -37,6 +41,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -56,8 +61,14 @@ require (
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
go.etcd.io/etcd/client/v3 v3.5.14 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.28.0 // indirect
@ -78,7 +89,7 @@ require (
k8s.io/api v0.31.0 // indirect
k8s.io/client-go v0.31.0 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

View File

@ -35,6 +35,7 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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=
@ -77,8 +78,8 @@ 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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db h1:mDk0bwRV6rDrLSmKXftcPf9kLA9uH6EvxJvzpPW9bso=
github.com/grafana/authlib/claims v0.0.0-20240814074258-eae7d47f01db/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus=
github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 h1:ItDcDxUjVLPKja+hogpqgW/kj8LxUL2qscelXIsN1Bs=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1/go.mod h1:DkxMin+qOh1Fgkxfbt+CUfBqqsCQJMG9op8Os/irBPA=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=

View File

@ -0,0 +1,57 @@
package generic
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// NewStatusREST makes a RESTStorage for status that has more limited options.
// It is based on the original REST so that we can share the same underlying store
func NewStatusREST(store *genericregistry.Store, strategy rest.UpdateResetFieldsStrategy) *StatusREST {
statusStore := *store
statusStore.CreateStrategy = nil
statusStore.DeleteStrategy = nil
statusStore.UpdateStrategy = strategy
statusStore.ResetFieldsStrategy = strategy
return &StatusREST{store: &statusStore}
}
// StatusREST implements the REST endpoint for changing the status of an DataPlaneService.
type StatusREST struct {
store *genericregistry.Store
}
var _ = rest.Patcher(&StatusREST{})
// New creates a new DataPlaneService object.
func (r *StatusREST) New() runtime.Object {
return r.store.NewFunc()
}
// Destroy cleans up resources on shutdown.
func (r *StatusREST) Destroy() {
// Given that underlying store is shared with REST,
// we don't destroy it here explicitly.
}
// Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options)
}
// Update alters the status subset of an object.
func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
// We are explicitly setting forceAllowCreate to false in the call to the underlying storage because
// subresources should never allow create on update.
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options)
}
// GetResetFields implements rest.ResetFieldsStrategy
func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
return r.store.GetResetFields()
}

View File

@ -3,57 +3,155 @@ package generic
import (
"context"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type genericStrategy struct {
runtime.ObjectTyper
names.NameGenerator
gv schema.GroupVersion
}
// NewStrategy creates and returns a genericStrategy instance.
func NewStrategy(typer runtime.ObjectTyper) genericStrategy {
return genericStrategy{typer, names.SimpleNameGenerator}
func NewStrategy(typer runtime.ObjectTyper, gv schema.GroupVersion) *genericStrategy {
return &genericStrategy{typer, names.SimpleNameGenerator, gv}
}
// NamespaceScoped returns true because all Generic resources must be within a namespace.
func (genericStrategy) NamespaceScoped() bool {
func (g *genericStrategy) NamespaceScoped() bool {
return true
}
func (genericStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {}
func (g *genericStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
fields := map[fieldpath.APIVersion]*fieldpath.Set{
fieldpath.APIVersion(g.gv.String()): fieldpath.NewSet(
fieldpath.MakePathOrDie("status"),
),
}
return fields
}
func (genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {}
func (g *genericStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {}
func (genericStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
func (g *genericStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
oldMeta, err := utils.MetaAccessor(old)
if err != nil {
return
}
status, err := oldMeta.GetStatus()
if err != nil {
return
}
newMeta, err := utils.MetaAccessor(obj)
if err != nil {
return
}
_ = newMeta.SetStatus(status)
}
func (g *genericStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
return field.ErrorList{}
}
// WarningsOnCreate returns warnings for the creation of the given object.
func (genericStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { return nil }
func (g *genericStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (genericStrategy) AllowCreateOnUpdate() bool {
func (g *genericStrategy) AllowCreateOnUpdate() bool {
return true
}
func (genericStrategy) AllowUnconditionalUpdate() bool {
func (g *genericStrategy) AllowUnconditionalUpdate() bool {
return true
}
func (genericStrategy) Canonicalize(obj runtime.Object) {}
func (g *genericStrategy) Canonicalize(obj runtime.Object) {}
func (genericStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
func (g *genericStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return field.ErrorList{}
}
// WarningsOnUpdate returns warnings for the given update.
func (genericStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
func (g *genericStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
type genericStatusStrategy struct {
runtime.ObjectTyper
names.NameGenerator
gv schema.GroupVersion
}
// NewStatusStrategy creates a new genericStatusStrategy.
func NewStatusStrategy(typer runtime.ObjectTyper, gv schema.GroupVersion) *genericStatusStrategy {
return &genericStatusStrategy{typer, names.SimpleNameGenerator, gv}
}
func (g *genericStatusStrategy) NamespaceScoped() bool {
return true
}
func (g *genericStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
fields := map[fieldpath.APIVersion]*fieldpath.Set{
fieldpath.APIVersion(g.gv.String()): fieldpath.NewSet(
fieldpath.MakePathOrDie("spec"),
fieldpath.MakePathOrDie("metadata"),
),
}
return fields
}
func (g *genericStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
oldMeta, err := utils.MetaAccessor(old)
if err != nil {
return
}
newMeta, err := utils.MetaAccessor(obj)
if err != nil {
return
}
newMeta.SetAnnotations(oldMeta.GetAnnotations())
newMeta.SetLabels(oldMeta.GetLabels())
newMeta.SetFinalizers(oldMeta.GetFinalizers())
newMeta.SetOwnerReferences(oldMeta.GetOwnerReferences())
}
func (g *genericStatusStrategy) AllowCreateOnUpdate() bool {
return false
}
func (g *genericStatusStrategy) AllowUnconditionalUpdate() bool {
return false
}
// Canonicalize normalizes the object after validation.
func (g *genericStatusStrategy) Canonicalize(obj runtime.Object) {
}
// ValidateUpdate validates an update of genericStatusStrategy.
func (g *genericStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return field.ErrorList{}
}
// WarningsOnUpdate returns warnings for the given update.
func (g *genericStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}

View File

@ -6,12 +6,15 @@ import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
)
@ -102,7 +105,13 @@ const (
// TODO: make this function private as there should only be one public way of setting the dual writing mode
// NewDualWriter returns a new DualWriter.
func NewDualWriter(mode DualWriterMode, legacy LegacyStorage, storage Storage, reg prometheus.Registerer, kind string) DualWriter {
func NewDualWriter(
mode DualWriterMode,
legacy LegacyStorage,
storage Storage,
reg prometheus.Registerer,
kind string,
) DualWriter {
metrics := &dualWriterMetrics{}
metrics.init(reg)
switch mode {
@ -148,6 +157,10 @@ type NamespacedKVStore interface {
Set(ctx context.Context, key, value string) error
}
type ServerLockService interface {
LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error
}
func SetDualWritingMode(
ctx context.Context,
kvs NamespacedKVStore,
@ -156,6 +169,8 @@ func SetDualWritingMode(
entity string,
desiredMode DualWriterMode,
reg prometheus.Registerer,
serverLockService ServerLockService,
requestInfo *request.RequestInfo,
) (DualWriterMode, error) {
// Mode0 means no DualWriter
if desiredMode == Mode0 {
@ -206,6 +221,7 @@ func SetDualWritingMode(
return Mode0, errDualWriterSetCurrentMode
}
}
if (desiredMode == Mode1) && (currentMode == Mode2) {
// This is where we go through the different gates to allow the instance to migrate from mode 2 to mode 1.
// There are none between mode 1 and mode 2
@ -217,6 +233,28 @@ func SetDualWritingMode(
}
}
if (desiredMode == Mode3) && (currentMode == Mode2) {
// This is where we go through the different gates to allow the instance to migrate from mode 2 to mode 3.
// gate #1: ensure the data is 100% in sync
syncOk, err := runDataSyncer(ctx, currentMode, legacy, storage, entity, reg, serverLockService, requestInfo)
if err != nil {
klog.Info("data syncer failed for mode:", m)
return currentMode, err
}
if !syncOk {
klog.Info("data syncer not ok for mode:", m)
return currentMode, nil
}
err = kvs.Set(ctx, entity, fmt.Sprint(desiredMode))
if err != nil {
return currentMode, errDualWriterSetCurrentMode
}
return desiredMode, nil
}
// #TODO add support for other combinations of desired and current modes
return currentMode, nil
@ -260,3 +298,54 @@ func getName(o runtime.Object) string {
}
return accessor.GetName()
}
const dataSyncerInterval = 60 * time.Minute
// StartPeriodicDataSyncer starts a background job that will execute the DataSyncer every 60 minutes
func StartPeriodicDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage,
kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) {
klog.Info("Starting periodic data syncer for mode mode: ", mode)
// run in background
go func() {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
timeWindow := 600 // 600 seconds (10 minutes)
jitterSeconds := r.Int63n(int64(timeWindow))
klog.Info("data syncer is going to start at: ", time.Now().Add(time.Second*time.Duration(jitterSeconds)))
time.Sleep(time.Second * time.Duration(jitterSeconds))
// run it immediately
syncOK, err := runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo)
klog.Info("data syncer finished, syncOK: ", syncOK, ", error: ", err)
ticker := time.NewTicker(dataSyncerInterval)
for {
select {
case <-ticker.C:
syncOK, err = runDataSyncer(ctx, mode, legacy, storage, kind, reg, serverLockService, requestInfo)
klog.Info("data syncer finished, syncOK: ", syncOK, ", error: ", err)
case <-ctx.Done():
return
}
}
}()
}
// runDataSyncer will ensure that data between legacy storage and unified storage are in sync.
// The sync implementation depends on the DualWriter mode
func runDataSyncer(ctx context.Context, mode DualWriterMode, legacy LegacyStorage, storage Storage,
kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) {
// ensure that execution takes no longer than necessary
const timeout = dataSyncerInterval - time.Minute
ctx, cancelFn := context.WithTimeout(ctx, timeout)
defer cancelFn()
// implementation depends on the current DualWriter mode
switch mode {
case Mode2:
return mode2DataSyncer(ctx, legacy, storage, kind, reg, serverLockService, requestInfo)
default:
klog.Info("data syncer not implemented for mode mode:", mode)
return false, nil
}
}

View File

@ -3,17 +3,22 @@ package rest
import (
"context"
"errors"
"fmt"
"time"
"github.com/prometheus/client_golang/prometheus"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
@ -30,7 +35,9 @@ const mode2Str = "2"
// NewDualWriterMode2 returns a new DualWriter in mode 2.
// Mode 2 represents writing to LegacyStorage and Storage and reading from LegacyStorage.
func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, kind string) *DualWriterMode2 {
return &DualWriterMode2{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode2").WithValues("mode", mode2Str, "kind", kind), dualWriterMetrics: dwm}
return &DualWriterMode2{
Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode2").WithValues("mode", mode2Str, "kind", kind), dualWriterMetrics: dwm,
}
}
// Mode returns the mode of the dual writer.
@ -394,3 +401,213 @@ func enrichLegacyObject(originalObj, returnedObj runtime.Object) error {
accessorReturned.SetUID(accessorOriginal.GetUID())
return nil
}
func getSyncRequester(orgId int64) *identity.StaticRequester {
return &identity.StaticRequester{
Type: claims.TypeServiceAccount, // system:apiserver
UserID: 1,
OrgID: orgId,
Name: "admin",
Login: "admin",
OrgRole: identity.RoleAdmin,
IsGrafanaAdmin: true,
Permissions: map[int64]map[string][]string{
orgId: {
"*": {"*"}, // all resources, all scopes
},
},
}
}
type syncItem struct {
name string
objStorage runtime.Object
objLegacy runtime.Object
}
func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalversion.ListOptions) ([]runtime.Object, error) {
ll, err := obj.List(ctx, listOptions)
if err != nil {
return nil, err
}
return meta.ExtractList(ll)
}
func mode2DataSyncer(ctx context.Context, legacy LegacyStorage, storage Storage, kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) {
metrics := &dualWriterMetrics{}
metrics.init(reg)
log := klog.NewKlogr().WithName("DualWriterMode2Syncer")
everythingSynced := false
outOfSync := 0
syncSuccess := 0
syncErr := 0
maxInterval := dataSyncerInterval + 5*time.Minute
var errSync error
const maxRecordsSync = 1000
// LockExecuteAndRelease ensures that just a single Grafana server acquires a lock at a time
// The parameter 'maxInterval' is a timeout safeguard, if the LastExecution in the
// database is older than maxInterval, we will assume the lock as timeouted. The 'maxInterval' parameter should be so long
// that is impossible for 2 processes to run at the same time.
err := serverLockService.LockExecuteAndRelease(ctx, "dualwriter mode 2 sync", maxInterval, func(context.Context) {
log.Info("starting dualwriter mode 2 sync")
startSync := time.Now()
orgId := int64(1)
ctx = klog.NewContext(ctx, log)
ctx = identity.WithRequester(ctx, getSyncRequester(orgId))
ctx = request.WithNamespace(ctx, requestInfo.Namespace)
ctx = request.WithRequestInfo(ctx, requestInfo)
storageList, err := getList(ctx, storage, &metainternalversion.ListOptions{
Limit: maxRecordsSync,
})
if err != nil {
log.Error(err, "unable to extract list from storage")
return
}
if len(storageList) >= maxRecordsSync {
errSync = fmt.Errorf("unified storage has more than %d records. Aborting sync", maxRecordsSync)
log.Error(errSync, "Unified storage has more records to be synced than allowed")
return
}
log.Info("got items from unified storage", "items", len(storageList))
legacyList, err := getList(ctx, legacy, &metainternalversion.ListOptions{})
if err != nil {
log.Error(err, "unable to extract list from legacy storage")
return
}
log.Info("got items from legacy storage", "items", len(legacyList))
itemsByName := map[string]syncItem{}
for _, obj := range legacyList {
accessor, err := utils.MetaAccessor(obj)
if err != nil {
log.Error(err, "error retrieving accessor data for object from legacy storage")
continue
}
name := accessor.GetName()
item, ok := itemsByName[name]
if !ok {
item = syncItem{}
}
item.name = name
item.objLegacy = obj
itemsByName[name] = item
}
for _, obj := range storageList {
accessor, err := utils.MetaAccessor(obj)
if err != nil {
log.Error(err, "error retrieving accessor data for object from storage")
continue
}
name := accessor.GetName()
item, ok := itemsByName[name]
if !ok {
item = syncItem{}
}
item.name = name
item.objStorage = obj
itemsByName[name] = item
}
log.Info("got list of items to be synced", "items", len(itemsByName))
for name, item := range itemsByName {
// upsert if:
// - existing in both legacy and storage, but objects are different, or
// - if it's missing from storage
if item.objLegacy != nil &&
((item.objStorage != nil && !Compare(item.objLegacy, item.objStorage)) || (item.objStorage == nil)) {
outOfSync++
accessor, err := utils.MetaAccessor(item.objLegacy)
if err != nil {
log.Error(err, "error retrieving accessor data for object from storage")
continue
}
if item.objStorage != nil {
accessorStorage, err := utils.MetaAccessor(item.objStorage)
if err != nil {
log.Error(err, "error retrieving accessor data for object from storage")
continue
}
accessor.SetResourceVersion(accessorStorage.GetResourceVersion())
accessor.SetUID(accessorStorage.GetUID())
log.Info("updating item on unified storage", "name", name)
} else {
accessor.SetResourceVersion("")
accessor.SetUID("")
log.Info("inserting item on unified storage", "name", name)
}
objInfo := rest.DefaultUpdatedObjectInfo(item.objLegacy, []rest.TransformFunc{}...)
res, _, err := storage.Update(ctx,
name,
objInfo,
func(ctx context.Context, obj runtime.Object) error { return nil },
func(ctx context.Context, obj, old runtime.Object) error { return nil },
true, // force creation
&metav1.UpdateOptions{},
)
if err != nil {
log.WithValues("object", res).Error(err, "could not update in storage")
syncErr++
} else {
syncSuccess++
}
}
// delete if object does not exists on legacy but exists on storage
if item.objLegacy == nil && item.objStorage != nil {
outOfSync++
ctx = request.WithRequestInfo(ctx, &request.RequestInfo{
APIGroup: requestInfo.APIGroup,
Resource: requestInfo.Resource,
Name: name,
Namespace: requestInfo.Namespace,
})
log.Info("deleting item from unified storage", "name", name)
deletedS, _, err := storage.Delete(ctx, name, func(ctx context.Context, obj runtime.Object) error { return nil }, &metav1.DeleteOptions{})
if err != nil {
if !apierrors.IsNotFound(err) {
log.WithValues("objectList", deletedS).Error(err, "could not delete from storage")
}
syncErr++
} else {
syncSuccess++
}
}
}
everythingSynced = outOfSync == syncSuccess
metrics.recordDataSyncerOutcome(mode2Str, kind, everythingSynced)
metrics.recordDataSyncerDuration(err != nil, mode2Str, kind, startSync)
log.Info("finished syncing items", "items", len(itemsByName), "updated", syncSuccess, "failed", syncErr, "outcome", everythingSynced)
})
if errSync != nil {
err = errSync
}
return everythingSynced, err
}

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -15,6 +16,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/apis/example"
"k8s.io/apiserver/pkg/endpoints/request"
)
var createFn = func(context.Context, runtime.Object) error { return nil }
@ -607,3 +609,197 @@ func TestEnrichReturnedObject(t *testing.T) {
})
}
}
var legacyObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var legacyObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var legacyObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var legacyObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var legacyObj2WithHostname = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{Hostname: "hostname"}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var storageObj1 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo1", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var storageObj2 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo2", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var storageObj3 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo3", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var storageObj4 = &example.Pod{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ObjectMeta: metav1.ObjectMeta{Name: "foo4", ResourceVersion: "1", CreationTimestamp: metav1.Time{}}, Spec: example.PodSpec{}, Status: example.PodStatus{StartTime: &metav1.Time{Time: time.Now()}}}
var legacyListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
Items: []example.Pod{
*legacyObj1,
*legacyObj2,
*legacyObj3,
}}
var legacyListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
Items: []example.Pod{
*legacyObj1,
*legacyObj2,
*legacyObj3,
*legacyObj4,
}}
var legacyListWith3itemsObj2IsDifferent = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
Items: []example.Pod{
*legacyObj1,
*legacyObj2WithHostname,
*legacyObj3,
}}
var storageListWith3items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
Items: []example.Pod{
*storageObj1,
*storageObj2,
*storageObj3,
}}
var storageListWith4items = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
Items: []example.Pod{
*storageObj1,
*storageObj2,
*storageObj3,
*storageObj4,
}}
var storageListWith3itemsMissingFoo2 = &example.PodList{TypeMeta: metav1.TypeMeta{Kind: "foo"}, ListMeta: metav1.ListMeta{},
Items: []example.Pod{
*storageObj1,
*storageObj3,
*storageObj4,
}}
func TestMode2_DataSyncer(t *testing.T) {
type testCase struct {
setupLegacyFn func(m *mock.Mock)
setupStorageFn func(m *mock.Mock)
name string
expectedOutcome bool
wantErr bool
}
tests :=
[]testCase{
{
name: "both stores are in sync",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
},
expectedOutcome: true,
},
{
name: "both stores are in sync - fail to list from legacy",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, errors.New("error"))
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
},
expectedOutcome: false,
},
{
name: "both stores are in sync - fail to list from storage",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, errors.New("error"))
},
expectedOutcome: false,
},
{
name: "storage is missing 1 entry (foo4)",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
},
expectedOutcome: true,
},
{
name: "storage needs to be update (foo2 is different)",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3itemsObj2IsDifferent, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
},
expectedOutcome: true,
},
{
name: "storage is missing 1 entry (foo4) - fail to upsert",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith4items, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3items, nil)
m.On("Update", mock.Anything, "foo4", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error"))
},
expectedOutcome: false,
},
{
name: "storage has an extra 1 entry (foo4)",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil)
m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil)
},
expectedOutcome: true,
},
{
name: "storage has an extra 1 entry (foo4) - fail to delete",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith4items, nil)
m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, errors.New("error"))
},
expectedOutcome: false,
},
{
name: "storage is missing 1 entry (foo3) and has an extra 1 entry (foo4)",
setupLegacyFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(legacyListWith3items, nil)
},
setupStorageFn: func(m *mock.Mock) {
m.On("List", mock.Anything, mock.Anything).Return(storageListWith3itemsMissingFoo2, nil)
m.On("Update", mock.Anything, "foo2", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(exampleObj, false, nil)
m.On("Delete", mock.Anything, "foo4", mock.Anything, mock.Anything).Return(exampleObj, false, nil)
},
expectedOutcome: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := (LegacyStorage)(nil)
s := (Storage)(nil)
lm := &mock.Mock{}
um := &mock.Mock{}
ls := legacyStoreMock{lm, l}
us := storageMock{um, s}
if tt.setupLegacyFn != nil {
tt.setupLegacyFn(lm)
}
if tt.setupStorageFn != nil {
tt.setupStorageFn(um)
}
outcome, err := mode2DataSyncer(context.Background(), ls, us, "test.kind", p, &fakeServerLock{}, &request.RequestInfo{})
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expectedOutcome, outcome)
})
}
}

View File

@ -6,12 +6,12 @@ import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/apis/example"
"k8s.io/apiserver/pkg/endpoints/request"
)
func TestSetDualWritingMode(t *testing.T) {
@ -43,13 +43,15 @@ func TestSetDualWritingMode(t *testing.T) {
s := (Storage)(nil)
m := &mock.Mock{}
m.On("List", mock.Anything, mock.Anything).Return(exampleList, nil)
m.On("List", mock.Anything, mock.Anything).Return(anotherList, nil)
ls := legacyStoreMock{m, l}
us := storageMock{m, s}
kvStore := &fakeNamespacedKV{data: make(map[string]string), namespace: "storage.dualwriting." + tt.stackID}
p := prometheus.NewRegistry()
dwMode, err := SetDualWritingMode(context.Background(), kvStore, ls, us, "playlist.grafana.app/v0alpha1", tt.desiredMode, p)
dwMode, err := SetDualWritingMode(context.Background(), kvStore, ls, us, "playlist.grafana.app/v0alpha1", tt.desiredMode, p, &fakeServerLock{}, &request.RequestInfo{})
assert.NoError(t, err)
assert.Equal(t, tt.expectedMode, dwMode)
@ -112,3 +114,12 @@ func (f *fakeNamespacedKV) Set(ctx context.Context, key, value string) error {
f.data[f.namespace+key] = value
return nil
}
// Never lock in tests
type fakeServerLock struct {
}
func (f *fakeServerLock) LockExecuteAndRelease(ctx context.Context, actionName string, duration time.Duration, fn func(ctx context.Context)) error {
fn(ctx)
return nil
}

View File

@ -12,6 +12,8 @@ type dualWriterMetrics struct {
legacy *prometheus.HistogramVec
storage *prometheus.HistogramVec
outcome *prometheus.HistogramVec
syncer *prometheus.HistogramVec
syncerOutcome *prometheus.HistogramVec
}
// DualWriterStorageDuration is a metric summary for dual writer storage duration per mode
@ -38,15 +40,41 @@ var DualWriterOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{
NativeHistogramBucketFactor: 1.1,
}, []string{"mode", "name", "method"})
var DualWriterReadLegacyCounts = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "dual_writer_read_legacy_count",
Help: "Histogram for the runtime of dual writer reads from legacy",
Namespace: "grafana",
}, []string{"kind", "method"})
// DualWriterSyncerDuration is a metric summary for dual writer sync duration per mode
var DualWriterSyncerDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "dual_writer_data_syncer_duration_seconds",
Help: "Histogram for the runtime of dual writer data syncer duration per mode",
Namespace: "grafana",
NativeHistogramBucketFactor: 1.1,
}, []string{"is_error", "mode", "kind"})
// DualWriterDataSyncerOutcome is a metric summary for dual writer data syncer outcome comparison between the 2 stores per mode
var DualWriterDataSyncerOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "dual_writer_data_syncer_outcome",
Help: "Histogram for the runtime of dual writer data syncer outcome comparison between the 2 stores per mode",
Namespace: "grafana",
NativeHistogramBucketFactor: 1.1,
}, []string{"mode", "kind"})
func (m *dualWriterMetrics) init(reg prometheus.Registerer) {
log := klog.NewKlogr()
m.legacy = DualWriterLegacyDuration
m.storage = DualWriterStorageDuration
m.outcome = DualWriterOutcome
m.syncer = DualWriterSyncerDuration
m.syncerOutcome = DualWriterDataSyncerOutcome
errLegacy := reg.Register(m.legacy)
errStorage := reg.Register(m.storage)
errOutcome := reg.Register(m.outcome)
if errLegacy != nil || errStorage != nil || errOutcome != nil {
errSyncer := reg.Register(m.syncer)
errSyncerOutcome := reg.Register(m.syncer)
if errLegacy != nil || errStorage != nil || errOutcome != nil || errSyncer != nil || errSyncerOutcome != nil {
log.Info("cloud migration metrics already registered")
}
}
@ -68,3 +96,16 @@ func (m *dualWriterMetrics) recordOutcome(mode string, name string, areEqual boo
}
m.outcome.WithLabelValues(mode, name, method).Observe(observeValue)
}
func (m *dualWriterMetrics) recordDataSyncerDuration(isError bool, mode string, kind string, startFrom time.Time) {
duration := time.Since(startFrom).Seconds()
m.syncer.WithLabelValues(strconv.FormatBool(isError), mode, kind).Observe(duration)
}
func (m *dualWriterMetrics) recordDataSyncerOutcome(mode string, kind string, synced bool) {
var observeValue float64
if !synced {
observeValue = 1
}
m.syncerOutcome.WithLabelValues(mode, kind).Observe(observeValue)
}

View File

@ -72,6 +72,7 @@ func WithUpdatedVersion(d *dagger.Client, src *dagger.Directory, nodeVersion, ve
WithExec([]string{"npm", "version", version, "--no-git-tag-version"}).
WithExec([]string{"yarn", "run", "lerna", "version", version, "--no-push", "--no-git-tag-version", "--force-publish", "--exact", "--yes"}).
WithExec([]string{"yarn", "install"}).
WithExec([]string{"yarn", "prettier:write"}).
Directory("/src").
WithoutDirectory("node_modules")
}

View File

@ -3,6 +3,7 @@ package commands
import (
"bufio"
"context"
"errors"
"fmt"
"os"
@ -16,6 +17,11 @@ import (
const DefaultAdminUserId = 1
var (
ErrMustBeAdmin = fmt.Errorf("reset-admin-password can only be used to reset an admin user account")
ErrAdminCannotBeFound = errors.New("admin user cannot be found")
)
func resetPasswordCommand(c utils.CommandLine, runner server.Runner) error {
var newPassword user.Password
adminId := int64(c.Int("user-id"))
@ -44,15 +50,35 @@ func resetPasswordCommand(c utils.CommandLine, runner server.Runner) error {
logger.Infof("\n")
logger.Infof("Admin password changed successfully %s", color.GreenString("✔"))
}
return err
if errors.Is(err, ErrAdminCannotBeFound) {
logger.Infof("\n")
logger.Infof("Admin user cannot be found %s. \n", color.RedString("✘"))
admins, err := listAdminUsers(runner.UserService)
if err != nil {
return fmt.Errorf("failed to list admin users: %w", err)
}
logger.Infof("\n")
logger.Infof("Please try to run the command again specifying a user-id (--user-id) from the list below:\n")
for _, u := range admins {
logger.Infof("\t Username: %s ID: %d\n", u.Login, u.ID)
}
}
return nil
}
func resetPassword(adminId int64, password user.Password, userSvc user.Service) error {
userQuery := user.GetUserByIDQuery{ID: adminId}
usr, err := userSvc.GetByID(context.Background(), &userQuery)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
return ErrAdminCannotBeFound
}
return fmt.Errorf("could not read user from database. Error: %v", err)
}
if !usr.IsAdmin {
return ErrMustBeAdmin
}
@ -64,4 +90,34 @@ func resetPassword(adminId int64, password user.Password, userSvc user.Service)
return nil
}
var ErrMustBeAdmin = fmt.Errorf("reset-admin-password can only be used to reset an admin user account")
func listAdminUsers(userSvc user.Service) ([]*user.UserSearchHitDTO, error) {
searchAdminsQuery := user.SearchUsersQuery{
Filters: []user.Filter{&adminFilter{}},
SignedInUser: &user.SignedInUser{
Permissions: map[int64]map[string][]string{0: {"users:read": {"global.users:*"}}},
},
}
admins, err := userSvc.Search(context.Background(), &searchAdminsQuery)
if err != nil {
return nil, fmt.Errorf("could not read user from database. Error: %v", err)
}
return admins.Users, nil
}
type adminFilter struct{}
func (f *adminFilter) WhereCondition() *user.WhereCondition {
return &user.WhereCondition{
Condition: "is_admin = 1",
}
}
func (f *adminFilter) JoinCondition() *user.JoinCondition {
return nil
}
func (f *adminFilter) InCondition() *user.InCondition {
return nil
}

View File

@ -167,7 +167,7 @@ func (o *APIServerOptions) RunAPIServer(ctx context.Context, config *genericapis
// Install the API Group+version
// #TODO figure out how to configure storage type in o.Options.StorageOptions
err = builder.InstallAPIs(grafanaAPIServer.Scheme, grafanaAPIServer.Codecs, server, config.RESTOptionsGetter, o.builders, o.Options.StorageOptions,
o.Options.MetricsOptions.MetricsRegisterer, nil, nil, // no need for server lock in standalone
o.Options.MetricsOptions.MetricsRegisterer, nil, nil, nil, // no need for server lock in standalone
)
if err != nil {
return err

View File

@ -35,7 +35,7 @@ func NewStorage(
tableConverter: resourceInfo.TableConverter(),
}
if optsGetter != nil && dualWriteBuilder != nil {
strategy := grafanaregistry.NewStrategy(scheme)
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
s := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,

View File

@ -41,7 +41,7 @@ func NewStorage(
tableConverter: resourceInfo.TableConverter(),
}
if optsGetter != nil && dualWriteBuilder != nil {
strategy := grafanaregistry.NewStrategy(scheme)
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
s := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,

View File

@ -9,7 +9,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
)
func TestQueries(t *testing.T) {
func TestDashboardQueries(t *testing.T) {
// prefix tables with grafana
nodb := &legacysql.LegacyDatabaseHelper{
Table: func(n string) string {

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
ORDER BY dashboard.id DESC

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.id > 22

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

View File

@ -9,11 +9,11 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard_version.version = 3

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM `grafana`.`library_element` as p
LEFT OUTER JOIN `grafana`.`user` AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.uid = 'xyz'
ORDER BY p.id DESC

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM `grafana`.`library_element` as p
LEFT OUTER JOIN `grafana`.`user` AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
ORDER BY p.id DESC
LIMIT 5

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM `grafana`.`library_element` as p
LEFT OUTER JOIN `grafana`.`user` AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.id > 4
ORDER BY p.id DESC

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
ORDER BY dashboard.id DESC

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.id > 22

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

View File

@ -9,11 +9,11 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard_version.version = 3

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM "grafana"."library_element" as p
LEFT OUTER JOIN "grafana"."user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.uid = 'xyz'
ORDER BY p.id DESC

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM "grafana"."library_element" as p
LEFT OUTER JOIN "grafana"."user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
ORDER BY p.id DESC
LIMIT 5

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM "grafana"."library_element" as p
LEFT OUTER JOIN "grafana"."user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.id > 4
ORDER BY p.id DESC

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
ORDER BY dashboard.id DESC

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.id > 22

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

View File

@ -9,11 +9,11 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard_version.version = 3

View File

@ -9,10 +9,10 @@ SELECT
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM "grafana.dashboard" as dashboard
LEFT OUTER JOIN "grafana.dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana.user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" as updated_user ON dashboard.updated_by = updated_user.id
FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM "grafana"."library_element" as p
LEFT OUTER JOIN "grafana"."user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.uid = 'xyz'
ORDER BY p.id DESC

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM "grafana"."library_element" as p
LEFT OUTER JOIN "grafana"."user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
ORDER BY p.id DESC
LIMIT 5

View File

@ -2,9 +2,9 @@ SELECT p.id, p.uid, p.folder_uid,
p.created, created_user.uid as created_by,
p.updated, updated_user.uid as updated_by,
p.name, p.type, p.description, p.model
FROM "grafana.library_element" as p
LEFT OUTER JOIN "grafana.user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana.user" AS updated_user ON p.updated_by = updated_user.id
FROM "grafana"."library_element" as p
LEFT OUTER JOIN "grafana"."user" AS created_user ON p.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" AS updated_user ON p.updated_by = updated_user.id
WHERE p.org_id = 1
AND p.id > 4
ORDER BY p.id DESC

View File

@ -47,7 +47,7 @@ func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter ge
defaultOpts.StorageConfig.Config,
)
strategy := grafanaregistry.NewStrategy(scheme)
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,

View File

@ -16,8 +16,8 @@ type storage struct {
}
func newStorage(scheme *runtime.Scheme) (*storage, error) {
strategy := grafanaregistry.NewStrategy(scheme)
resourceInfo := dashboard.DashboardResourceInfo
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
store := &genericregistry.Store{
NewFunc: resourceInfo.NewFunc,
NewListFunc: resourceInfo.NewListFunc,

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