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

pull/89331/head
Ryan McKinley 11 months ago
commit 1857690bd0
  1. 29
      .betterer.results
  2. 61
      .drone.yml
  3. 2
      .github/workflows/release-pr.yml
  4. 1
      docs/sources/introduction/grafana-enterprise.md
  5. 13
      packages/grafana-sql/src/components/ConfirmModal.tsx
  6. 22
      packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.tsx
  7. 4
      packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
  8. 40
      packages/grafana-ui/src/themes/GlobalStyles/code.ts
  9. 58
      packages/grafana-ui/src/themes/GlobalStyles/fonts.ts
  10. 2
      pkg/api/index.go
  11. 2
      pkg/infra/tracing/tracing.go
  12. 2
      pkg/infra/tracing/tracing_config.go
  13. 1
      pkg/services/apiserver/standalone/options/tracing.go
  14. 3
      pkg/services/licensing/oss.go
  15. 121
      pkg/services/navtree/models.go
  16. 170
      pkg/services/navtree/navtreeimpl/admin.go
  17. 4
      pkg/services/navtree/navtreeimpl/applinks.go
  18. 2
      pkg/services/navtree/navtreeimpl/navtree.go
  19. 30
      pkg/services/store/auth.go
  20. 3
      pkg/services/store/entity/sqlstash/utils.go
  21. 3
      pkg/services/store/entity/tests/server_integration_test.go
  22. 15
      public/app/features/browse-dashboards/components/CreateNewButton.tsx
  23. 8
      public/app/features/dashboard-scene/panel-edit/PanelOptions.test.tsx
  24. 2
      public/app/features/dashboard-scene/panel-edit/PanelOptions.tsx
  25. 33
      public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx
  26. 31
      public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx
  27. 31
      public/app/features/datasources/components/DataSourceTestingStatus.tsx
  28. 2
      public/app/features/datasources/state/buildCategories.test.ts
  29. 6
      public/app/features/datasources/state/buildCategories.ts
  30. 18
      public/app/features/inspector/InspectStatsTable.tsx
  31. 18
      public/app/features/inspector/InspectStatsTraceIdsTable.tsx
  32. 2
      public/app/features/inspector/QueryInspector.tsx
  33. 116
      public/app/features/inspector/styles.ts
  34. 13
      public/app/features/invites/SignupInvited.tsx
  35. 40
      public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx
  36. 1
      public/app/plugins/datasource/cloudwatch/defaultQueries.ts
  37. 15
      public/app/plugins/datasource/graphite/components/AnnotationsEditor.tsx
  38. 1691
      public/img/plugins/catchpoint.svg
  39. 40
      public/sass/_angular.scss
  40. 5
      public/sass/_grafana.scss
  41. 62
      public/sass/base/_code.scss
  42. 49
      public/sass/base/_fonts.scss
  43. 4
      public/sass/components/_tabbed_view.scss
  44. 50
      public/sass/utils/_spacings.scss
  45. 40
      scripts/drone/events/release.star

@ -3630,13 +3630,11 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
], ],
"public/app/features/datasources/components/DataSourceTestingStatus.tsx:5381": [ "public/app/features/datasources/components/DataSourceTestingStatus.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"]
], ],
"public/app/features/datasources/components/DataSourceTypeCard.tsx:5381": [ "public/app/features/datasources/components/DataSourceTypeCard.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
@ -4541,14 +4539,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"] [0, 0, 0, "Styles should be written using objects.", "1"]
], ],
"public/app/features/inspector/InspectStatsTable.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/inspector/InspectStatsTraceIdsTable.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/inspector/QueryInspector.tsx:5381": [ "public/app/features/inspector/QueryInspector.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "1"],
@ -4559,19 +4549,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "7"] [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "7"]
], ],
"public/app/features/inspector/styles.ts:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"],
[0, 0, 0, "Styles should be written using objects.", "5"],
[0, 0, 0, "Styles should be written using objects.", "6"],
[0, 0, 0, "Styles should be written using objects.", "7"],
[0, 0, 0, "Styles should be written using objects.", "8"],
[0, 0, 0, "Styles should be written using objects.", "9"],
[0, 0, 0, "Styles should be written using objects.", "10"]
],
"public/app/features/invites/InviteeRow.tsx:5381": [ "public/app/features/invites/InviteeRow.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"] [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
], ],

@ -2731,6 +2731,47 @@ volumes:
clone: clone:
retries: 3 retries: 3
depends_on: [] depends_on: []
image_pull_secrets:
- gcr
- gar
kind: pipeline
name: create-release-pr
node:
type: no-parallel
platform:
arch: amd64
os: linux
services: []
steps:
- commands:
- apk add perl
- v_target=`echo $${TAG} | perl -pe 's/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/v\1.\2.x/'`
- default_target=`if [[ -n $$LATEST ]]; then echo 'main'; else echo $$v_target;
fi`
- backport=`if [[ -n $$LATEST ]]; then echo $$v_target; fi`
- curl -L $${GH_CLI_URL} | tar -xz --strip-components=1 -C /usr
- gh workflow run -f dry_run=$${DRY_RUN} -f version=$${TAG} -f target=$${TARGET:-$default_target}
-f backport=$${BACKPORT:-$default_backport} --repo=grafana/grafana release-pr.yml
depends_on: []
environment:
GH_CLI_URL: https://github.com/cli/cli/releases/download/v2.50.0/gh_2.50.0_linux_amd64.tar.gz
GITHUB_TOKEN:
from_secret: github_token
image: byrnedo/alpine-curl:0.1.8
name: create-release-pr
trigger:
event:
- promote
target: release-pr
type: docker
volumes:
- host:
path: /var/run/docker.sock
name: docker
---
clone:
retries: 3
depends_on: []
environment: environment:
EDITION: oss EDITION: oss
image_pull_secrets: image_pull_secrets:
@ -2787,6 +2828,24 @@ steps:
from_secret: prerelease_bucket from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.3 image: grafana/grafana-ci-deploy:1.3.3
name: publish-storybook name: publish-storybook
- commands:
- apk add perl
- v_target=`echo $${TAG} | perl -pe 's/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/v\1.\2.x/'`
- default_target=`if [[ -n $$LATEST ]]; then echo 'main'; else echo $$v_target;
fi`
- backport=`if [[ -n $$LATEST ]]; then echo $$v_target; fi`
- curl -L $${GH_CLI_URL} | tar -xz --strip-components=1 -C /usr
- gh workflow run -f dry_run=$${DRY_RUN} -f version=$${TAG} -f target=$${TARGET:-$default_target}
-f backport=$${BACKPORT:-$default_backport} --repo=grafana/grafana release-pr.yml
depends_on:
- publish-artifacts
- publish-static-assets
environment:
GH_CLI_URL: https://github.com/cli/cli/releases/download/v2.50.0/gh_2.50.0_linux_amd64.tar.gz
GITHUB_TOKEN:
from_secret: github_token
image: byrnedo/alpine-curl:0.1.8
name: create-release-pr
trigger: trigger:
event: event:
- promote - promote
@ -4893,6 +4952,6 @@ kind: secret
name: gcr_credentials name: gcr_credentials
--- ---
kind: signature kind: signature
hmac: 08f38b820f97302de03a9fdfd39fb12c185bb36170704cf7591c16f33c3e4d31 hmac: 043028c50d984e1ea98a294c6746df1388cb0b7d7976f82f3dd0004fc493bafc
... ...

@ -57,6 +57,6 @@ jobs:
- name: Create PR with backports - name: Create PR with backports
if: "${{ github.event.inputs.backport != '' }}" if: "${{ github.event.inputs.backport != '' }}"
run: > run: >
gh pr create -l "backport-${{ inputs.backport }}" --dry-run=${{ inputs.dry_run }} -H "release/${{ inputs.version }}" -B "${{ inputs.target }}" --title "Release: ${{ inputs.version }}" --body "These code changes must be merged after a release is complete" gh pr create -l "backport ${{ inputs.backport }}" --dry-run=${{ inputs.dry_run }} -H "release/${{ inputs.version }}" -B "${{ inputs.target }}" --title "Release: ${{ inputs.version }}" --body "These code changes must be merged after a release is complete"
env: env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }} GH_TOKEN: ${{ steps.generate_token.outputs.token }}

@ -77,6 +77,7 @@ With a Grafana Enterprise license, you also get access to premium data sources,
- [AppDynamics](/grafana/plugins/dlopes7-appdynamics-datasource) - [AppDynamics](/grafana/plugins/dlopes7-appdynamics-datasource)
- [Azure CosmosDB](/grafana/plugins/grafana-azurecosmosdb-datasource) - [Azure CosmosDB](/grafana/plugins/grafana-azurecosmosdb-datasource)
- [Azure Devops](/grafana/plugins/grafana-azuredevops-datasource) - [Azure Devops](/grafana/plugins/grafana-azuredevops-datasource)
- [Catchpoint](/grafana/plugins/grafana-catchpoint-datasource)
- [Databricks](/grafana/plugins/grafana-databricks-datasource) - [Databricks](/grafana/plugins/grafana-databricks-datasource)
- [DataDog](/grafana/plugins/grafana-datadog-datasource) - [DataDog](/grafana/plugins/grafana-datadog-datasource)
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource) - [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)

@ -1,6 +1,8 @@
import { css } from '@emotion/css';
import React, { useRef, useEffect } from 'react'; import React, { useRef, useEffect } from 'react';
import { Button, Icon, Modal } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, Icon, Modal, useStyles2 } from '@grafana/ui';
type ConfirmModalProps = { type ConfirmModalProps = {
isOpen: boolean; isOpen: boolean;
@ -10,6 +12,7 @@ type ConfirmModalProps = {
}; };
export function ConfirmModal({ isOpen, onCancel, onDiscard, onCopy }: ConfirmModalProps) { export function ConfirmModal({ isOpen, onCancel, onDiscard, onCopy }: ConfirmModalProps) {
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const styles = useStyles2(getStyles);
// Moved from grafana/ui // Moved from grafana/ui
useEffect(() => { useEffect(() => {
@ -24,7 +27,7 @@ export function ConfirmModal({ isOpen, onCancel, onDiscard, onCopy }: ConfirmMod
title={ title={
<div className="modal-header-title"> <div className="modal-header-title">
<Icon name="exclamation-triangle" size="lg" /> <Icon name="exclamation-triangle" size="lg" />
<span className="p-l-1">Warning</span> <span className={styles.titleText}>Warning</span>
</div> </div>
} }
onDismiss={onCancel} onDismiss={onCancel}
@ -49,3 +52,9 @@ export function ConfirmModal({ isOpen, onCancel, onDiscard, onCopy }: ConfirmMod
</Modal> </Modal>
); );
} }
const getStyles = (theme: GrafanaTheme2) => ({
titleText: css({
paddingLeft: theme.spacing(2),
}),
});

@ -1,10 +1,10 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { useState, useCallback, useId } from 'react'; import React, { useState, useCallback, useId } from 'react';
import { SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { useTheme2 } from '../../themes'; import { useStyles2, useTheme2 } from '../../themes';
import { FormField } from '../FormField/FormField'; import { FormField } from '../FormField/FormField';
import { InlineFormLabel } from '../FormLabel/FormLabel'; import { InlineFormLabel } from '../FormLabel/FormLabel';
import { InlineField } from '../Forms/InlineField'; import { InlineField } from '../Forms/InlineField';
@ -37,8 +37,10 @@ const DEFAULT_ACCESS_OPTION = {
value: 'proxy', value: 'proxy',
}; };
const HttpAccessHelp = () => ( const HttpAccessHelp = () => {
<div className="grafana-info-box m-t-2"> const styles = useStyles2(getAccessStyles);
return (
<div className={cx('grafana-info-box', styles.infoBox)}>
<p> <p>
Access mode controls how requests to the data source will be handled. Access mode controls how requests to the data source will be handled.
<strong> <strong>
@ -54,11 +56,19 @@ const HttpAccessHelp = () => (
</p> </p>
<div className="alert-title">Browser access mode:</div> <div className="alert-title">Browser access mode:</div>
<p> <p>
All requests will be made from the browser directly to the data source and may be subject to Cross-Origin Resource All requests will be made from the browser directly to the data source and may be subject to Cross-Origin
Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access mode. Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access
mode.
</p> </p>
</div> </div>
); );
};
const getAccessStyles = (theme: GrafanaTheme2) => ({
infoBox: css({
marginTop: theme.spacing(3),
}),
});
const LABEL_WIDTH = 26; const LABEL_WIDTH = 26;

@ -5,8 +5,10 @@ import { useTheme2 } from '../ThemeContext';
import { getAgularPanelStyles } from './angularPanelStyles'; import { getAgularPanelStyles } from './angularPanelStyles';
import { getCardStyles } from './card'; import { getCardStyles } from './card';
import { getCodeStyles } from './code';
import { getElementStyles } from './elements'; import { getElementStyles } from './elements';
import { getExtraStyles } from './extra'; import { getExtraStyles } from './extra';
import { getFontStyles } from './fonts';
import { getFormElementStyles } from './forms'; import { getFormElementStyles } from './forms';
import { getLegacySelectStyles } from './legacySelect'; import { getLegacySelectStyles } from './legacySelect';
import { getMarkdownStyles } from './markdownStyles'; import { getMarkdownStyles } from './markdownStyles';
@ -22,8 +24,10 @@ export function GlobalStyles() {
return ( return (
<Global <Global
styles={[ styles={[
getCodeStyles(theme),
getElementStyles(theme), getElementStyles(theme),
getExtraStyles(theme), getExtraStyles(theme),
getFontStyles(theme),
getFormElementStyles(theme), getFormElementStyles(theme),
getPageStyles(theme), getPageStyles(theme),
getCardStyles(theme), getCardStyles(theme),

@ -0,0 +1,40 @@
import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
export function getCodeStyles(theme: GrafanaTheme2) {
return css({
'code, pre': {
...theme.typography.code,
fontSize: theme.typography.bodySmall.fontSize,
backgroundColor: theme.colors.background.primary,
color: theme.colors.text.primary,
border: `1px solid ${theme.colors.border.medium}`,
borderRadius: '4px',
},
code: {
whiteSpace: 'nowrap',
padding: '2px 5px',
margin: '0 2px',
},
pre: {
display: 'block',
margin: `0 0 ${theme.typography.body.lineHeight}`,
lineHeight: theme.typography.body.lineHeight,
wordBreak: 'break-all',
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
padding: '10px',
code: {
padding: 0,
color: 'inherit',
whiteSpace: 'pre-wrap',
backgroundColor: 'transparent',
border: 0,
},
},
});
}

@ -0,0 +1,58 @@
import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
export function getFontStyles(theme: GrafanaTheme2) {
return css([
{
/* latin */
'@font-face': {
fontFamily: 'Roboto Mono',
fontStyle: 'normal',
fontWeight: 400,
fontDisplay: 'swap',
src: "url('./public/fonts/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')",
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
},
{
/* latin */
'@font-face': {
fontFamily: 'Roboto Mono',
fontStyle: 'normal',
fontWeight: 500,
fontDisplay: 'swap',
src: "url('./public/fonts/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2') format('woff2')",
unicodeRange:
'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD',
},
},
{
/*
To add new variations/version of Inter, download from https://rsms.me/inter/ and add the
web font files to the public/fonts/inter folder. Do not download the fonts from Google Fonts
or somewhere else because they don't support the features we require (like tabular numerals).
If adding additional weights, consider switching to the InterVariable variable font as combined
it may take less space than multiple static weights.
*/
'@font-face': {
fontFamily: 'Inter',
fontStyle: 'normal',
fontWeight: 400,
fontDisplay: 'swap',
src: "url('./public/fonts/inter/Inter-Regular.woff2') format('woff2')",
},
},
{
'@font-face': {
fontFamily: 'Inter',
fontStyle: 'normal',
fontWeight: 500,
fontDisplay: 'swap',
src: "url('./public/fonts/inter/Inter-Medium.woff2') format('woff2')",
},
},
]);
}

@ -158,7 +158,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
hs.HooksService.RunIndexDataHooks(&data, c) hs.HooksService.RunIndexDataHooks(&data, c)
data.NavTree.ApplyAdminIA() data.NavTree.ApplyCostManagementIA()
data.NavTree.ApplyHelpVersion(data.Settings.BuildInfo.VersionString) // RunIndexDataHooks can modify the version string data.NavTree.ApplyHelpVersion(data.Settings.BuildInfo.VersionString) // RunIndexDataHooks can modify the version string
data.NavTree.Sort() data.NavTree.Sort()

@ -272,7 +272,9 @@ func (ots *TracingService) initOpentelemetryTracer() error {
} }
} }
if ots.cfg.ProfilingIntegration {
tp = NewProfilingTracerProvider(tp) tp = NewProfilingTracerProvider(tp)
}
// Register our TracerProvider as the global so any imported // Register our TracerProvider as the global so any imported
// instrumentation in the future will default to using it // instrumentation in the future will default to using it

@ -21,6 +21,8 @@ type TracingConfig struct {
ServiceName string ServiceName string
ServiceVersion string ServiceVersion string
ProfilingIntegration bool
} }
func ProvideTracingConfig(cfg *setting.Cfg) (*TracingConfig, error) { func ProvideTracingConfig(cfg *setting.Cfg) (*TracingConfig, error) {

@ -109,6 +109,7 @@ func (o *TracingOptions) ApplyTo(config *genericapiserver.RecommendedConfig) err
tracingCfg.Sampler = o.SamplerType tracingCfg.Sampler = o.SamplerType
tracingCfg.SamplerParam = o.SamplerParam tracingCfg.SamplerParam = o.SamplerParam
tracingCfg.SamplerRemoteURL = o.SamplingServiceURL tracingCfg.SamplerRemoteURL = o.SamplingServiceURL
tracingCfg.ProfilingIntegration = true
ts, err := tracing.ProvideService(tracingCfg) ts, err := tracing.ProvideService(tracingCfg)
if err != nil { if err != nil {

@ -59,12 +59,13 @@ func ProvideService(cfg *setting.Cfg, hooksService *hooks.HooksService) *OSSLice
return return
} }
if adminNode := indexData.NavTree.FindById(navtree.NavIDCfg); adminNode != nil { if adminNode := indexData.NavTree.FindById(navtree.NavIDCfgGeneral); adminNode != nil {
adminNode.Children = append(adminNode.Children, &navtree.NavLink{ adminNode.Children = append(adminNode.Children, &navtree.NavLink{
Text: "Stats and license", Text: "Stats and license",
Id: "upgrading", Id: "upgrading",
Url: l.LicenseURL(req.IsGrafanaAdmin), Url: l.LicenseURL(req.IsGrafanaAdmin),
Icon: "unlock", Icon: "unlock",
SortWeight: -1,
}) })
} }
}) })

@ -136,113 +136,50 @@ func (root *NavTreeRoot) ApplyHelpVersion(version string) {
} }
} }
func (root *NavTreeRoot) ApplyAdminIA() { func (root *NavTreeRoot) ApplyCostManagementIA() {
orgAdminNode := root.FindById(NavIDCfg) orgAdminNode := root.FindById(NavIDCfg)
var costManagementApp *NavLink
var adaptiveMetricsApp *NavLink
var attributionsApp *NavLink
var logVolumeExplorerApp *NavLink
if orgAdminNode != nil { if orgAdminNode != nil {
adminNodeLinks := []*NavLink{} adminNodeLinks := []*NavLink{}
for _, element := range orgAdminNode.Children {
generalNodeLinks := []*NavLink{} switch navId := element.Id; navId {
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("upgrading")) // TODO does this even exist case "plugin-page-grafana-costmanagementui-app":
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("licensing")) costManagementApp = element
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("org-settings")) case "plugin-page-grafana-adaptive-metrics-app":
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("server-settings")) adaptiveMetricsApp = element
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("global-orgs")) case "plugin-page-grafana-attributions-app":
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("feature-toggles")) attributionsApp = element
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("storage")) case "plugin-page-grafana-logvolumeexplorer-app":
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("migrate-to-cloud")) logVolumeExplorerApp = element
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("banner-settings")) default:
adminNodeLinks = append(adminNodeLinks, element)
generalNode := &NavLink{
Text: "General",
SubTitle: "Manage default preferences and settings across Grafana",
Id: NavIDCfgGeneral,
Url: "/admin/general",
Icon: "shield",
Children: generalNodeLinks,
} }
pluginsNodeLinks := []*NavLink{}
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("plugins"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("datasources"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("recordedQueries"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("correlations"))
pluginsNodeLinks = AppendIfNotNil(pluginsNodeLinks, root.FindById("plugin-page-grafana-cloud-link-app"))
pluginsNode := &NavLink{
Text: "Plugins and data",
SubTitle: "Install plugins and define the relationships between data",
Id: NavIDCfgPlugins,
Url: "/admin/plugins",
Icon: "shield",
Children: pluginsNodeLinks,
} }
accessNodeLinks := []*NavLink{} if costManagementApp != nil {
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("global-users")) costManagementMetricsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/metrics")
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("teams")) if costManagementMetricsNode != nil {
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("standalone-plugin-page-/a/grafana-auth-app")) if adaptiveMetricsApp != nil {
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("serviceaccounts")) costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, adaptiveMetricsApp)
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("apikeys"))
usersNode := &NavLink{
Text: "Users and access",
SubTitle: "Configure access for individual users, teams, and service accounts",
Id: NavIDCfgAccess,
Url: "/admin/access",
Icon: "shield",
Children: accessNodeLinks,
} }
if attributionsApp != nil {
if len(generalNode.Children) > 0 { costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, attributionsApp)
adminNodeLinks = append(adminNodeLinks, generalNode)
} }
if len(pluginsNode.Children) > 0 {
adminNodeLinks = append(adminNodeLinks, pluginsNode)
} }
if len(usersNode.Children) > 0 { costManagementLogsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/logs")
adminNodeLinks = append(adminNodeLinks, usersNode) if costManagementLogsNode != nil {
if logVolumeExplorerApp != nil {
costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerApp)
} }
authenticationNode := root.FindById("authentication")
if authenticationNode != nil {
authenticationNode.IsSection = true
adminNodeLinks = append(adminNodeLinks, authenticationNode)
} }
adminNodeLinks = append(adminNodeLinks, costManagementApp)
costManagementNode := root.FindById("plugin-page-grafana-costmanagementui-app")
if costManagementNode != nil {
adminNodeLinks = append(adminNodeLinks, costManagementNode)
} }
costManagementMetricsNode := root.FindByURL("/a/grafana-costmanagementui-app/metrics")
adaptiveMetricsNode := root.FindById("plugin-page-grafana-adaptive-metrics-app")
if costManagementMetricsNode != nil && adaptiveMetricsNode != nil {
costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, adaptiveMetricsNode)
}
attributionsNode := root.FindById("plugin-page-grafana-attributions-app")
if costManagementMetricsNode != nil && attributionsNode != nil {
costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, attributionsNode)
}
costManagementLogsNode := root.FindByURL("/a/grafana-costmanagementui-app/logs")
logVolumeExplorerNode := root.FindById("plugin-page-grafana-logvolumeexplorer-app")
if costManagementLogsNode != nil && logVolumeExplorerNode != nil {
costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerNode)
}
if len(adminNodeLinks) > 0 {
orgAdminNode.Children = adminNodeLinks orgAdminNode.Children = adminNodeLinks
} else {
root.RemoveSection(orgAdminNode)
}
} }
} }

@ -21,10 +21,71 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead) orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
authConfigUIAvailable := s.license.FeatureEnabled(social.SAMLProviderName) || s.cfg.LDAPAuthEnabled authConfigUIAvailable := s.license.FeatureEnabled(social.SAMLProviderName) || s.cfg.LDAPAuthEnabled
generalNodeLinks := []*navtree.NavLink{}
if hasAccess(ac.OrgPreferencesAccessEvaluator) {
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Default preferences",
Id: "org-settings",
SubTitle: "Manage preferences across an organization",
Icon: "sliders-v-alt",
Url: s.cfg.AppSubURL + "/org",
})
}
if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) {
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Settings", SubTitle: "View the settings defined in your Grafana config", Id: "server-settings", Url: s.cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
})
}
if hasGlobalAccess(orgsAccessEvaluator) {
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
})
}
if s.features.IsEnabled(ctx, featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) {
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Feature Toggles",
SubTitle: "View and edit feature toggles",
Id: "feature-toggles",
Url: s.cfg.AppSubURL + "/admin/featuretoggles",
Icon: "toggle-on",
})
}
if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(ctx, featuremgmt.FlagStorage) {
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Storage",
Id: "storage",
SubTitle: "Manage file storage",
Icon: "cube",
Url: s.cfg.AppSubURL + "/admin/storage",
})
}
if s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) && c.SignedInUser.HasRole(org.RoleAdmin) {
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Migrate to Grafana Cloud",
Id: "migrate-to-cloud",
SubTitle: "Copy configuration from your self-managed installation to a cloud stack",
Url: s.cfg.AppSubURL + "/admin/migrate-to-cloud",
})
}
generalNode := &navtree.NavLink{
Text: "General",
SubTitle: "Manage default preferences and settings across Grafana",
Id: navtree.NavIDCfgGeneral,
Url: "/admin/general",
Icon: "shield",
Children: generalNodeLinks,
}
if len(generalNode.Children) > 0 {
configNodes = append(configNodes, generalNode)
}
pluginsNodeLinks := []*navtree.NavLink{}
// FIXME: If plugin admin is disabled or externally managed, server admins still need to access the page, this is why // FIXME: If plugin admin is disabled or externally managed, server admins still need to access the page, this is why
// while we don't have a permissions for listing plugins the legacy check has to stay as a default // while we don't have a permissions for listing plugins the legacy check has to stay as a default
if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) { if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{ pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
Text: "Plugins", Text: "Plugins",
Id: "plugins", Id: "plugins",
SubTitle: "Extend the Grafana experience with plugins", SubTitle: "Extend the Grafana experience with plugins",
@ -32,15 +93,37 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Url: s.cfg.AppSubURL + "/plugins", Url: s.cfg.AppSubURL + "/plugins",
}) })
} }
if s.features.IsEnabled(ctx, featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) {
pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
Text: "Correlations",
Icon: "gf-glue",
SubTitle: "Add and configure correlations",
Id: "correlations",
Url: s.cfg.AppSubURL + "/datasources/correlations",
})
}
pluginsNode := &navtree.NavLink{
Text: "Plugins and data",
SubTitle: "Install plugins and define the relationships between data",
Id: navtree.NavIDCfgPlugins,
Url: "/admin/plugins",
Icon: "shield",
Children: pluginsNodeLinks,
}
if len(pluginsNode.Children) > 0 {
configNodes = append(configNodes, pluginsNode)
}
accessNodeLinks := []*navtree.NavLink{}
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll))) { if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll))) {
configNodes = append(configNodes, &navtree.NavLink{ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "Users", SubTitle: "Manage users in Grafana", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user", Text: "Users", SubTitle: "Manage users in Grafana", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user",
}) })
} }
if hasAccess(ac.TeamsAccessEvaluator) { if hasAccess(ac.TeamsAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "Teams", Text: "Teams",
Id: "teams", Id: "teams",
SubTitle: "Groups of users that have common dashboard and permission needs", SubTitle: "Groups of users that have common dashboard and permission needs",
@ -48,9 +131,8 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Url: s.cfg.AppSubURL + "/org/teams", Url: s.cfg.AppSubURL + "/org/teams",
}) })
} }
if enableServiceAccount(s, c) { if enableServiceAccount(s, c) {
configNodes = append(configNodes, &navtree.NavLink{ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "Service accounts", Text: "Service accounts",
Id: "serviceaccounts", Id: "serviceaccounts",
SubTitle: "Use service accounts to run automated workloads in Grafana", SubTitle: "Use service accounts to run automated workloads in Grafana",
@ -58,13 +140,12 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Url: s.cfg.AppSubURL + "/org/serviceaccounts", Url: s.cfg.AppSubURL + "/org/serviceaccounts",
}) })
} }
disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID()) disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID())
if err != nil { if err != nil {
return nil, err return nil, err
} }
if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled { if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled {
configNodes = append(configNodes, &navtree.NavLink{ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "API keys", Text: "API keys",
Id: "apikeys", Id: "apikeys",
SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs", SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs",
@ -73,14 +154,17 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
}) })
} }
if hasAccess(ac.OrgPreferencesAccessEvaluator) { usersNode := &navtree.NavLink{
configNodes = append(configNodes, &navtree.NavLink{ Text: "Users and access",
Text: "Default preferences", SubTitle: "Configure access for individual users, teams, and service accounts",
Id: "org-settings", Id: navtree.NavIDCfgAccess,
SubTitle: "Manage preferences across an organization", Url: "/admin/access",
Icon: "sliders-v-alt", Icon: "shield",
Url: s.cfg.AppSubURL + "/org", Children: accessNodeLinks,
}) }
if len(usersNode.Children) > 0 {
configNodes = append(configNodes, usersNode)
} }
if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) || if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) ||
@ -90,63 +174,11 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Id: "authentication", Id: "authentication",
SubTitle: "Manage your auth settings and configure single sign-on", SubTitle: "Manage your auth settings and configure single sign-on",
Icon: "signin", Icon: "signin",
IsSection: true,
Url: s.cfg.AppSubURL + "/admin/authentication", Url: s.cfg.AppSubURL + "/admin/authentication",
}) })
} }
if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Settings", SubTitle: "View the settings defined in your Grafana config", Id: "server-settings", Url: s.cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
})
}
if hasGlobalAccess(orgsAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
})
}
if s.features.IsEnabled(ctx, featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Feature Toggles",
SubTitle: "View and edit feature toggles",
Id: "feature-toggles",
Url: s.cfg.AppSubURL + "/admin/featuretoggles",
Icon: "toggle-on",
})
}
if s.features.IsEnabled(ctx, featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Correlations",
Icon: "gf-glue",
SubTitle: "Add and configure correlations",
Id: "correlations",
Url: s.cfg.AppSubURL + "/datasources/correlations",
})
}
if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(ctx, featuremgmt.FlagStorage) {
storage := &navtree.NavLink{
Text: "Storage",
Id: "storage",
SubTitle: "Manage file storage",
Icon: "cube",
Url: s.cfg.AppSubURL + "/admin/storage",
}
configNodes = append(configNodes, storage)
}
if s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) && c.SignedInUser.HasRole(org.RoleAdmin) {
migrateToCloud := &navtree.NavLink{
Text: "Migrate to Grafana Cloud",
Id: "migrate-to-cloud",
SubTitle: "Copy configuration from your self-managed installation to a cloud stack",
Url: s.cfg.AppSubURL + "/admin/migrate-to-cloud",
}
configNodes = append(configNodes, migrateToCloud)
}
configNode := &navtree.NavLink{ configNode := &navtree.NavLink{
Id: navtree.NavIDCfg, Id: navtree.NavIDCfg,
Text: "Administration", Text: "Administration",

@ -295,7 +295,7 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"}, "grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"}, "grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"},
"grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4}, "grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4},
"grafana-cloud-link-app": {SectionID: navtree.NavIDCfg}, "grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3},
"grafana-costmanagementui-app": {SectionID: navtree.NavIDCfg, Text: "Cost management"}, "grafana-costmanagementui-app": {SectionID: navtree.NavIDCfg, Text: "Cost management"},
"grafana-adaptive-metrics-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Metrics"}, "grafana-adaptive-metrics-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Metrics"},
"grafana-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"}, "grafana-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"},
@ -307,7 +307,7 @@ func (s *ServiceImpl) readNavigationSettings() {
} }
s.navigationAppPathConfig = map[string]NavigationAppConfig{ s.navigationAppPathConfig = map[string]NavigationAppConfig{
"/a/grafana-auth-app": {SectionID: navtree.NavIDCfg, SortWeight: 7}, "/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2},
} }
appSections := s.cfg.Raw.Section("navigation.app_sections") appSections := s.cfg.Raw.Section("navigation.app_sections")

@ -151,7 +151,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere
orgAdminNode, err := s.getAdminNode(c) orgAdminNode, err := s.getAdminNode(c)
if orgAdminNode != nil { if orgAdminNode != nil && len(orgAdminNode.Children) > 0 {
treeRoot.AddSection(orgAdminNode) treeRoot.AddSection(orgAdminNode)
} else if err != nil { } else if err != nil {
return nil, err return nil, err

@ -1,30 +0,0 @@
package store
import (
"fmt"
"github.com/grafana/grafana/pkg/services/user"
)
// Really just spitballing here :) this should hook into a system that can give better display info
func GetUserIDString(user *user.SignedInUser) string {
// TODO: should we check IsDisabled?
// TODO: could we use the NamespacedID.ID() as prefix instead of manually
// setting "anon", "key", etc.?
// TODO: the default unauthenticated user is not anonymous and would be
// returned as `sys:0:` here. We may want to do something special in that
// case
if user == nil {
return ""
}
if user.IsAnonymous {
return "anon"
}
if user.ApiKeyID > 0 {
return fmt.Sprintf("key:%d", user.UserID)
}
if user.IsRealUser() {
return fmt.Sprintf("user:%d:%s", user.UserID, user.Login)
}
return fmt.Sprintf("sys:%d:%s", user.UserID, user.Login)
}

@ -9,7 +9,6 @@ import (
"text/template" "text/template"
"github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity/db" "github.com/grafana/grafana/pkg/services/store/entity/db"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate" "github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
) )
@ -33,7 +32,7 @@ func getCurrentUser(ctx context.Context) (string, error) {
return "", fmt.Errorf("%w: %w", ErrUserNotFoundInContext, err) return "", fmt.Errorf("%w: %w", ErrUserNotFoundInContext, err)
} }
return store.GetUserIDString(user), nil return user.GetUID().String(), nil
} }
// ptrOr returns the first non-nil pointer in the list or a new non-nil pointer. // ptrOr returns the first non-nil pointer in the list or a new non-nil pointer.

@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/appcontext" "github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/entity"
) )
@ -122,7 +121,7 @@ func TestIntegrationEntityServer(t *testing.T) {
testCtx := createTestContext(t) testCtx := createTestContext(t)
ctx := appcontext.WithUser(testCtx.ctx, testCtx.user) ctx := appcontext.WithUser(testCtx.ctx, testCtx.user)
fakeUser := store.GetUserIDString(testCtx.user) fakeUser := testCtx.user.GetUID().String()
firstVersion := int64(0) firstVersion := int64(0)
group := "test.grafana.app" group := "test.grafana.app"
resource := "jsonobjs" resource := "jsonobjs"

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { reportInteraction } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui'; import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { import {
getNewDashboardPhrase, getNewDashboardPhrase,
@ -50,11 +50,11 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC
label={getNewDashboardPhrase()} label={getNewDashboardPhrase()}
onClick={() => onClick={() =>
reportInteraction('grafana_menu_item_clicked', { reportInteraction('grafana_menu_item_clicked', {
url: addFolderUidToUrl('/dashboard/new', parentFolder?.uid), url: buildUrl('/dashboard/new', parentFolder?.uid),
from: location.pathname, from: location.pathname,
}) })
} }
url={addFolderUidToUrl('/dashboard/new', parentFolder?.uid)} url={buildUrl('/dashboard/new', parentFolder?.uid)}
/> />
)} )}
{canCreateFolder && <MenuItem onClick={() => setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />} {canCreateFolder && <MenuItem onClick={() => setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />}
@ -63,11 +63,11 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC
label={getImportPhrase()} label={getImportPhrase()}
onClick={() => onClick={() =>
reportInteraction('grafana_menu_item_clicked', { reportInteraction('grafana_menu_item_clicked', {
url: addFolderUidToUrl('/dashboard/import', parentFolder?.uid), url: buildUrl('/dashboard/import', parentFolder?.uid),
from: location.pathname, from: location.pathname,
}) })
} }
url={addFolderUidToUrl('/dashboard/import', parentFolder?.uid)} url={buildUrl('/dashboard/import', parentFolder?.uid)}
/> />
)} )}
</Menu> </Menu>
@ -101,6 +101,7 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC
* @param folderUid folder id * @param folderUid folder id
* @returns url with paramter if folder is present * @returns url with paramter if folder is present
*/ */
function addFolderUidToUrl(url: string, folderUid: string | undefined) { function buildUrl(url: string, folderUid: string | undefined) {
return folderUid ? url + '?folderUid=' + folderUid : url; const baseUrl = folderUid ? url + '?folderUid=' + folderUid : url;
return config.appSubUrl ? config.appSubUrl + baseUrl : baseUrl;
} }

@ -152,6 +152,14 @@ describe('PanelOptions', () => {
expect(screen.queryByLabelText(overrideRuleTooltipDescription)).not.toBeInTheDocument(); expect(screen.queryByLabelText(overrideRuleTooltipDescription)).not.toBeInTheDocument();
}); });
it('Can delete rule', async () => {
const {} = setup();
await userEvent.click(screen.getByLabelText('Remove override'));
expect(screen.queryByLabelText(overrideRuleTooltipDescription)).not.toBeInTheDocument();
});
}); });
it('gets library panel options when the editing a library panel', async () => { it('gets library panel options when the editing a library panel', async () => {

@ -62,7 +62,7 @@ export const PanelOptions = React.memo<Props>(({ vizManager, searchQuery, listMo
data?.series ?? [], data?.series ?? [],
searchQuery, searchQuery,
(newConfig) => { (newConfig) => {
panel.onFieldConfigChange(newConfig); panel.onFieldConfigChange(newConfig, true);
} }
), ),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

@ -1,5 +1,6 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import React from 'react'; import React from 'react';
import { finalize, from, Subscription } from 'rxjs';
import { Scope } from '@grafana/data'; import { Scope } from '@grafana/data';
import { import {
@ -33,6 +34,8 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] }); protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] });
private nodesFetchingSub: Subscription | undefined;
get scopesParent(): ScopesScene { get scopesParent(): ScopesScene {
return sceneGraph.getAncestor(this, ScopesScene); return sceneGraph.getAncestor(this, ScopesScene);
} }
@ -61,6 +64,10 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
this.addActivationHandler(() => { this.addActivationHandler(() => {
this.fetchBaseNodes(); this.fetchBaseNodes();
return () => {
this.nodesFetchingSub?.unsubscribe();
};
}); });
} }
@ -80,6 +87,8 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
} }
public async updateNode(path: string[], isExpanded: boolean, query: string) { public async updateNode(path: string[], isExpanded: boolean, query: string) {
this.nodesFetchingSub?.unsubscribe();
let nodes = { ...this.state.nodes }; let nodes = { ...this.state.nodes };
let currentLevel: NodesMap = nodes; let currentLevel: NodesMap = nodes;
@ -90,16 +99,30 @@ export class ScopesFiltersScene extends SceneObjectBase<ScopesFiltersSceneState>
const name = path[path.length - 1]; const name = path[path.length - 1];
const currentNode = currentLevel[name]; const currentNode = currentLevel[name];
if (isExpanded || currentNode.query !== query) { const isDifferentQuery = currentNode.query !== query;
this.setState({ loadingNodeName: name });
currentNode.nodes = await fetchNodes(name, query);
}
currentNode.isExpanded = isExpanded; currentNode.isExpanded = isExpanded;
currentNode.query = query; currentNode.query = query;
this.setState({ nodes, loadingNodeName: undefined }); this.setState({ nodes, loadingNodeName: undefined });
if (isExpanded || isDifferentQuery) {
this.setState({ loadingNodeName: name });
this.nodesFetchingSub = from(fetchNodes(name, query))
.pipe(
finalize(() => {
this.setState({ loadingNodeName: undefined });
})
)
.subscribe((childNodes) => {
currentNode.nodes = childNodes;
this.setState({ nodes });
this.nodesFetchingSub?.unsubscribe();
});
}
} }
public toggleNodeSelect(path: string[]) { public toggleNodeSelect(path: string[]) {

@ -1,6 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React from 'react'; import React, { useMemo } from 'react';
import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Checkbox, Icon, IconButton, Input, useStyles2 } from '@grafana/ui'; import { Checkbox, Icon, IconButton, Input, useStyles2 } from '@grafana/ui';
@ -33,28 +34,31 @@ export function ScopesTreeLevel({
const node = nodes[nodeId]; const node = nodes[nodeId];
const childNodes = node.nodes; const childNodes = node.nodes;
const childNodesArr = Object.values(childNodes); const childNodesArr = Object.values(childNodes);
const isNodeLoading = loadingNodeName === nodeId;
const anyChildExpanded = childNodesArr.some(({ isExpanded }) => isExpanded); const anyChildExpanded = childNodesArr.some(({ isExpanded }) => isExpanded);
const anyChildSelected = childNodesArr.some(({ linkId }) => linkId && scopeNames.includes(linkId!)); const anyChildSelected = childNodesArr.some(({ linkId }) => linkId && scopeNames.includes(linkId!));
const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]);
return ( return (
<> <>
{showQuery && !anyChildExpanded && ( {showQuery && !anyChildExpanded && (
<Input <Input
prefix={<Icon name="filter" />} prefix={<Icon name="filter" />}
className={styles.searchInput} className={styles.searchInput}
disabled={!!loadingNodeName}
placeholder={t('scopes.tree.search', 'Filter')} placeholder={t('scopes.tree.search', 'Filter')}
defaultValue={node.query} defaultValue={node.query}
data-testid={`scopes-tree-${nodeId}-search`} data-testid={`scopes-tree-${nodeId}-search`}
onChange={debounce((evt) => { onInput={(evt) => onQueryUpdate(nodePath, true, evt.currentTarget.value)}
onNodeUpdate(nodePath, true, evt.target.value);
}, 500)}
/> />
)} )}
<div role="tree"> <div role="tree">
{childNodesArr.map((childNode) => { {isNodeLoading && <Skeleton count={5} className={styles.loader} />}
{!isNodeLoading &&
childNodesArr.map((childNode) => {
const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!); const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
if (anyChildExpanded && !childNode.isExpanded && !isSelected) { if (anyChildExpanded && !childNode.isExpanded && !isSelected) {
@ -69,7 +73,7 @@ export function ScopesTreeLevel({
{childNode.isSelectable && !childNode.isExpanded ? ( {childNode.isSelectable && !childNode.isExpanded ? (
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={!!loadingNodeName || (anyChildSelected && !isSelected && node.disableMultiSelect)} disabled={anyChildSelected && !isSelected && node.disableMultiSelect}
data-testid={`scopes-tree-${childNode.name}-checkbox`} data-testid={`scopes-tree-${childNode.name}-checkbox`}
onChange={() => { onChange={() => {
onNodeSelectToggle(childNodePath); onNodeSelectToggle(childNodePath);
@ -79,14 +83,8 @@ export function ScopesTreeLevel({
{childNode.isExpandable && ( {childNode.isExpandable && (
<IconButton <IconButton
disabled={(anyChildSelected && !childNode.isExpanded) || !!loadingNodeName} disabled={anyChildSelected && !childNode.isExpanded}
name={ name={!childNode.isExpanded ? 'angle-right' : 'angle-down'}
!childNode.isExpanded
? 'angle-right'
: loadingNodeName === childNode.name
? 'spinner'
: 'angle-down'
}
aria-label={ aria-label={
childNode.isExpanded ? t('scopes.tree.collapse', 'Collapse') : t('scopes.tree.expand', 'Expand') childNode.isExpanded ? t('scopes.tree.collapse', 'Collapse') : t('scopes.tree.expand', 'Expand')
} }
@ -126,6 +124,9 @@ const getStyles = (theme: GrafanaTheme2) => {
searchInput: css({ searchInput: css({
margin: theme.spacing(1, 0), margin: theme.spacing(1, 0),
}), }),
loader: css({
margin: theme.spacing(0.5, 0),
}),
itemTitle: css({ itemTitle: css({
alignItems: 'center', alignItems: 'center',
display: 'flex', display: 'flex',

@ -4,7 +4,7 @@ import React, { HTMLAttributes } from 'react';
import { DataSourceSettings as DataSourceSettingsType, GrafanaTheme2 } from '@grafana/data'; import { DataSourceSettings as DataSourceSettingsType, GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { TestingStatus, config } from '@grafana/runtime'; import { TestingStatus, config } from '@grafana/runtime';
import { AlertVariant, Alert, useTheme2, Link } from '@grafana/ui'; import { AlertVariant, Alert, useTheme2, Link, useStyles2 } from '@grafana/ui';
import { contextSrv } from '../../../core/core'; import { contextSrv } from '../../../core/core';
import { trackCreateDashboardClicked } from '../tracking'; import { trackCreateDashboardClicked } from '../tracking';
@ -25,16 +25,16 @@ interface AlertMessageProps extends HTMLAttributes<HTMLDivElement> {
const getStyles = (theme: GrafanaTheme2, hasTitle: boolean) => { const getStyles = (theme: GrafanaTheme2, hasTitle: boolean) => {
return { return {
content: css` content: css({
color: ${theme.colors.text.secondary}; color: theme.colors.text.secondary,
padding-top: ${hasTitle ? theme.spacing(1) : 0}; paddingTop: hasTitle ? theme.spacing(1) : 0,
max-height: 50vh; maxHeight: '50vh',
overflow-y: auto; overflowY: 'auto',
`, }),
disabled: css` disabled: css({
pointer-events: none; pointerEvents: 'none',
color: ${theme.colors.text.secondary}; color: theme.colors.text.secondary,
`, }),
}; };
}; };
@ -95,10 +95,11 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource
path: location.pathname, path: location.pathname,
}); });
}; };
const styles = useStyles2(getTestingStatusStyles);
if (message) { if (message) {
return ( return (
<div className="gf-form-group p-t-2"> <div className={cx('gf-form-group', styles.container)}>
<Alert severity={severity} title={message} data-testid={e2eSelectors.pages.DataSource.alert}> <Alert severity={severity} title={message} data-testid={e2eSelectors.pages.DataSource.alert}>
{testingStatus?.details && ( {testingStatus?.details && (
<> <>
@ -123,3 +124,9 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource
return null; return null;
} }
const getTestingStatusStyles = (theme: GrafanaTheme2) => ({
container: css({
paddingTop: theme.spacing(3),
}),
});

@ -53,7 +53,7 @@ describe('buildCategories', () => {
it('should add enterprise phantom plugins', () => { it('should add enterprise phantom plugins', () => {
const enterprisePluginsCategory = categories[3]; const enterprisePluginsCategory = categories[3];
expect(enterprisePluginsCategory.title).toBe('Enterprise plugins'); expect(enterprisePluginsCategory.title).toBe('Enterprise plugins');
expect(enterprisePluginsCategory.plugins.length).toBe(20); expect(enterprisePluginsCategory.plugins.length).toBe(21);
expect(enterprisePluginsCategory.plugins[0].name).toBe('AppDynamics'); expect(enterprisePluginsCategory.plugins[0].name).toBe('AppDynamics');
expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Wavefront'); expect(enterprisePluginsCategory.plugins[enterprisePluginsCategory.plugins.length - 1].name).toBe('Wavefront');
}); });

@ -209,6 +209,12 @@ function getEnterprisePhantomPlugins(): DataSourcePluginMeta[] {
name: 'PagerDuty', name: 'PagerDuty',
imgUrl: 'public/img/plugins/pagerduty.svg', imgUrl: 'public/img/plugins/pagerduty.svg',
}), }),
getPhantomPlugin({
id: 'grafana-catchpoint-datasource',
description: 'Catchpoint datasource',
name: 'Catchpoint',
imgUrl: 'public/img/plugins/catchpoint.svg',
}),
getPhantomPlugin({ getPhantomPlugin({
id: 'grafana-azurecosmosdb-datasource', id: 'grafana-azurecosmosdb-datasource',
description: 'Azure CosmosDB datasource', description: 'Azure CosmosDB datasource',

@ -27,7 +27,7 @@ export const InspectStatsTable = ({ timeZone, name, stats }: InspectStatsTablePr
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className="section-heading">{name}</div> <div className={styles.heading}>{name}</div>
<table className="filter-table width-30"> <table className="filter-table width-30">
<tbody> <tbody>
{stats.map((stat, index) => { {stats.map((stat, index) => {
@ -57,10 +57,14 @@ function formatStat(stat: QueryResultMetaStat, timeZone: TimeZone, theme: Grafan
} }
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css` heading: css({
padding-bottom: ${theme.spacing(2)}; fontSize: theme.typography.body.fontSize,
`, marginBottom: theme.spacing(1),
cell: css` }),
text-align: right; wrapper: css({
`, paddingBottom: theme.spacing(2),
}),
cell: css({
textAlign: 'right',
}),
}); });

@ -18,7 +18,7 @@ export const InspectStatsTraceIdsTable = ({ name, traceIds }: Props) => {
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className="section-heading">{name}</div> <div className={styles.heading}>{name}</div>
<table className="filter-table width-30"> <table className="filter-table width-30">
<tbody> <tbody>
{traceIds.map((traceId, index) => { {traceIds.map((traceId, index) => {
@ -35,10 +35,14 @@ export const InspectStatsTraceIdsTable = ({ name, traceIds }: Props) => {
}; };
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css` heading: css({
padding-bottom: ${theme.spacing(2)}; fontSize: theme.typography.body.fontSize,
`, marginBottom: theme.spacing(1),
cell: css` }),
text-align: right; wrapper: css({
`, paddingBottom: theme.spacing(2),
}),
cell: css({
textAlign: 'right',
}),
}); });

@ -228,7 +228,7 @@ export class QueryInspector extends PureComponent<Props, State> {
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
<div aria-label={selectors.components.PanelInspector.Query.content}> <div aria-label={selectors.components.PanelInspector.Query.content}>
<h3 className="section-heading">Query inspector</h3> <h3 className={styles.heading}>Query inspector</h3>
<p className="small muted"> <p className="small muted">
<Trans i18nKey="inspector.query.description"> <Trans i18nKey="inspector.query.description">
Query inspector allows you to view raw request and response. To collect this data Grafana needs to issue a Query inspector allows you to view raw request and response. To collect this data Grafana needs to issue a

@ -11,62 +11,66 @@ export const getPanelInspectorStyles = stylesFactory(() => {
export const getPanelInspectorStyles2 = (theme: GrafanaTheme2) => { export const getPanelInspectorStyles2 = (theme: GrafanaTheme2) => {
return { return {
wrap: css` heading: css({
display: flex; fontSize: theme.typography.body.fontSize,
flex-direction: column; marginBottom: theme.spacing(1),
height: 100%; }),
width: 100%; wrap: css({
flex: 1 1 0; display: 'flex',
min-height: 0; flexDirection: 'column',
`, height: '100%',
toolbar: css` width: '100%',
display: flex; flex: '1 1 0',
width: 100%; minHeight: 0,
flex-grow: 0; }),
align-items: center; toolbar: css({
justify-content: flex-end; display: 'flex',
margin-bottom: ${theme.v1.spacing.sm}; width: '100%',
`, flexGrow: 0,
toolbarItem: css` alignItems: 'center',
margin-left: ${theme.v1.spacing.md}; justifyContent: 'flex-end',
`, marginBottom: theme.v1.spacing.sm,
content: css` }),
flex-grow: 1; toolbarItem: css({
height: 100%; marginLeft: theme.v1.spacing.md,
`, }),
editor: css` content: css({
font-family: monospace; flexGrow: 1,
height: 100%; height: '100%',
flex-grow: 1; }),
`, editor: css({
viewer: css` fontFamily: 'monospace',
overflow: scroll; height: '100%',
`, flexGrow: 1,
dataFrameSelect: css` }),
flex-grow: 2; viewer: css({
`, overflow: 'scroll',
leftActions: css` }),
display: flex; dataFrameSelect: css({
flex-grow: 1; flexGrow: 2,
}),
leftActions: css({
display: 'flex',
flexGrow: 1,
max-width: 85%; maxWidth: '85%',
@media (max-width: 1345px) { '@media (max-width: 1345px)': {
max-width: 75%; maxWidth: '75%',
} },
`, }),
options: css` options: css({
padding-top: ${theme.v1.spacing.sm}; paddingTop: theme.v1.spacing.sm,
`, }),
dataDisplayOptions: css` dataDisplayOptions: css({
flex-grow: 1; flexGrow: 1,
min-width: 300px; minWidth: '300px',
margin-right: ${theme.v1.spacing.sm}; marginRight: theme.v1.spacing.sm,
`, }),
selects: css` selects: css({
display: flex; display: 'flex',
> * { '> *': {
margin-right: ${theme.v1.spacing.sm}; marginRight: theme.v1.spacing.sm,
} },
`, }),
}; };
}; };

@ -1,8 +1,10 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { Button, Field, Input } from '@grafana/ui'; import { Button, Field, Input, useStyles2 } from '@grafana/ui';
import { Form } from 'app/core/components/Form/Form'; import { Form } from 'app/core/components/Form/Form';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { getConfig } from 'app/core/config'; import { getConfig } from 'app/core/config';
@ -37,6 +39,7 @@ export const SignupInvitedPage = ({ match }: Props) => {
const [initFormModel, setInitFormModel] = useState<FormModel>(); const [initFormModel, setInitFormModel] = useState<FormModel>();
const [greeting, setGreeting] = useState<string>(); const [greeting, setGreeting] = useState<string>();
const [invitedBy, setInvitedBy] = useState<string>(); const [invitedBy, setInvitedBy] = useState<string>();
const styles = useStyles2(getStyles);
useAsync(async () => { useAsync(async () => {
const invite = await getBackendSrv().get(`/api/user/invite/${code}`); const invite = await getBackendSrv().get(`/api/user/invite/${code}`);
@ -65,7 +68,7 @@ export const SignupInvitedPage = ({ match }: Props) => {
<Page.Contents> <Page.Contents>
<h3 className="page-sub-heading">Hello {greeting || 'there'}.</h3> <h3 className="page-sub-heading">Hello {greeting || 'there'}.</h3>
<div className="modal-tagline p-b-2"> <div className={cx('modal-tagline', styles.tagline)}>
<em>{invitedBy || 'Someone'}</em> has invited you to join Grafana and the organization{' '} <em>{invitedBy || 'Someone'}</em> has invited you to join Grafana and the organization{' '}
<span className="highlight-word">{contextSrv.user.orgName}</span> <span className="highlight-word">{contextSrv.user.orgName}</span>
<br /> <br />
@ -109,4 +112,10 @@ export const SignupInvitedPage = ({ match }: Props) => {
); );
}; };
const getStyles = (theme: GrafanaTheme2) => ({
tagline: css({
paddingBottom: theme.spacing(3),
}),
});
export default SignupInvitedPage; export default SignupInvitedPage;

@ -5,6 +5,7 @@ import { EditorField, EditorRow, InlineSelect } from '@grafana/experimental';
import { ConfirmModal, Input, RadioButtonGroup, Space } from '@grafana/ui'; import { ConfirmModal, Input, RadioButtonGroup, Space } from '@grafana/ui';
import { CloudWatchDatasource } from '../../../datasource'; import { CloudWatchDatasource } from '../../../datasource';
import { DEFAULT_METRICS_QUERY } from '../../../defaultQueries';
import useMigratedMetricsQuery from '../../../migrations/useMigratedMetricsQuery'; import useMigratedMetricsQuery from '../../../migrations/useMigratedMetricsQuery';
import { import {
CloudWatchJsonData, CloudWatchJsonData,
@ -39,13 +40,13 @@ const editorModes = [
export const MetricsQueryEditor = (props: Props) => { export const MetricsQueryEditor = (props: Props) => {
const { query, datasource, extraHeaderElementLeft, extraHeaderElementRight, onChange } = props; const { query, datasource, extraHeaderElementLeft, extraHeaderElementRight, onChange } = props;
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
const [sqlCodeEditorIsDirty, setSQLCodeEditorIsDirty] = useState(false); const [codeEditorIsDirty, setCodeEditorIsDirty] = useState(false);
const migratedQuery = useMigratedMetricsQuery(query, props.onChange); const migratedQuery = useMigratedMetricsQuery(query, props.onChange);
const onEditorModeChange = useCallback( const onEditorModeChange = useCallback(
(newMetricEditorMode: MetricEditorMode) => { (newMetricEditorMode: MetricEditorMode) => {
if ( if (
sqlCodeEditorIsDirty && codeEditorIsDirty &&
query.metricQueryType === MetricQueryType.Query && query.metricQueryType === MetricQueryType.Query &&
query.metricEditorMode === MetricEditorMode.Code query.metricEditorMode === MetricEditorMode.Code
) { ) {
@ -54,7 +55,7 @@ export const MetricsQueryEditor = (props: Props) => {
} }
onChange({ ...query, metricEditorMode: newMetricEditorMode }); onChange({ ...query, metricEditorMode: newMetricEditorMode });
}, },
[setShowConfirm, onChange, sqlCodeEditorIsDirty, query] [setShowConfirm, onChange, codeEditorIsDirty, query]
); );
useEffect(() => { useEffect(() => {
@ -64,6 +65,14 @@ export const MetricsQueryEditor = (props: Props) => {
value={metricEditorModes.find((m) => m.value === query.metricQueryType)} value={metricEditorModes.find((m) => m.value === query.metricQueryType)}
options={metricEditorModes} options={metricEditorModes}
onChange={({ value }) => { onChange={({ value }) => {
if (
codeEditorIsDirty &&
query.metricQueryType === MetricQueryType.Search &&
query.metricEditorMode === MetricEditorMode.Builder
) {
setShowConfirm(true);
return;
}
onChange({ ...query, metricQueryType: value }); onChange({ ...query, metricQueryType: value });
}} }}
/> />
@ -80,13 +89,19 @@ export const MetricsQueryEditor = (props: Props) => {
<ConfirmModal <ConfirmModal
isOpen={showConfirm} isOpen={showConfirm}
title="Are you sure?" title="Are you sure?"
body="You will lose manual changes done to the query if you go back to the visual builder." body="You will lose changes made to the query if you change to Metric Query Builder mode."
confirmText="Yes, I am sure." confirmText="Yes, I am sure."
dismissText="No, continue editing the query manually." dismissText="No, continue editing the query."
icon="exclamation-triangle" icon="exclamation-triangle"
onConfirm={() => { onConfirm={() => {
setShowConfirm(false); setShowConfirm(false);
onChange({ ...query, metricEditorMode: MetricEditorMode.Builder }); setCodeEditorIsDirty(false);
onChange({
...query,
...DEFAULT_METRICS_QUERY,
metricQueryType: MetricQueryType.Query,
metricEditorMode: MetricEditorMode.Builder,
});
}} }}
onDismiss={() => setShowConfirm(false)} onDismiss={() => setShowConfirm(false)}
/> />
@ -99,7 +114,7 @@ export const MetricsQueryEditor = (props: Props) => {
}; };
}, [ }, [
query, query,
sqlCodeEditorIsDirty, codeEditorIsDirty,
datasource, datasource,
onChange, onChange,
extraHeaderElementLeft, extraHeaderElementLeft,
@ -119,7 +134,12 @@ export const MetricsQueryEditor = (props: Props) => {
{...props} {...props}
refId={query.refId} refId={query.refId}
metricStat={query} metricStat={query}
onChange={(metricStat: MetricStat) => props.onChange({ ...query, ...metricStat })} onChange={(metricStat: MetricStat) => {
if (!codeEditorIsDirty) {
setCodeEditorIsDirty(true);
}
props.onChange({ ...query, ...metricStat });
}}
></MetricStatEditor> ></MetricStatEditor>
)} )}
{query.metricEditorMode === MetricEditorMode.Code && ( {query.metricEditorMode === MetricEditorMode.Code && (
@ -138,8 +158,8 @@ export const MetricsQueryEditor = (props: Props) => {
region={query.region} region={query.region}
sql={query.sqlExpression ?? ''} sql={query.sqlExpression ?? ''}
onChange={(sqlExpression) => { onChange={(sqlExpression) => {
if (!sqlCodeEditorIsDirty) { if (!codeEditorIsDirty) {
setSQLCodeEditorIsDirty(true); setCodeEditorIsDirty(true);
} }
props.onChange({ ...migratedQuery, sqlExpression }); props.onChange({ ...migratedQuery, sqlExpression });
}} }}

@ -21,6 +21,7 @@ export const DEFAULT_METRICS_QUERY: Omit<CloudWatchMetricsQuery, 'refId'> = {
period: '', period: '',
metricQueryType: MetricQueryType.Search, metricQueryType: MetricQueryType.Search,
metricEditorMode: MetricEditorMode.Builder, metricEditorMode: MetricEditorMode.Builder,
sql: undefined,
sqlExpression: '', sqlExpression: '',
matchExact: true, matchExact: true,
}; };

@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { QueryEditorProps } from '@grafana/data'; import { GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { Box, InlineField, Input, TagsInput } from '@grafana/ui'; import { Box, InlineField, Input, TagsInput, useStyles2 } from '@grafana/ui';
import { GraphiteDatasource } from '../datasource'; import { GraphiteDatasource } from '../datasource';
import { GraphiteQuery, GraphiteOptions } from '../types'; import { GraphiteQuery, GraphiteOptions } from '../types';
@ -32,6 +33,7 @@ export const AnnotationEditor = (props: QueryEditorProps<GraphiteDatasource, Gra
setTags(tagsInput); setTags(tagsInput);
updateValue('tags', tagsInput); updateValue('tags', tagsInput);
}; };
const styles = useStyles2(getStyles);
return ( return (
<Box marginBottom={5}> <Box marginBottom={5}>
@ -44,7 +46,7 @@ export const AnnotationEditor = (props: QueryEditorProps<GraphiteDatasource, Gra
/> />
</InlineField> </InlineField>
<h5 className="section-heading">Or</h5> <h5 className={styles.heading}>Or</h5>
<InlineField label="Graphite events tags" labelWidth={24}> <InlineField label="Graphite events tags" labelWidth={24}>
<TagsInput id="tags-input" width={50} tags={tags} onChange={onTagsChange} placeholder="Example: event_tag" /> <TagsInput id="tags-input" width={50} tags={tags} onChange={onTagsChange} placeholder="Example: event_tag" />
@ -52,3 +54,10 @@ export const AnnotationEditor = (props: QueryEditorProps<GraphiteDatasource, Gra
</Box> </Box>
); );
}; };
const getStyles = (theme: GrafanaTheme2) => ({
heading: css({
fontSize: theme.typography.body.fontSize,
marginBottom: theme.spacing(1),
}),
});

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 38 KiB

@ -1,6 +1,8 @@
// these styles are only used by angular components/pages // these styles are only used by angular components/pages
// once angular is disabled, this file can be deleted // once angular is disabled, this file can be deleted
@use 'sass:map';
.edit-tab-content { .edit-tab-content {
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
@ -1792,3 +1794,41 @@ $easing: cubic-bezier(0, 0, 0.265, 1);
} }
} }
} }
.section-heading {
font-size: $font-size-md;
margin-bottom: $space-sm;
}
@each $prop, $abbrev in (margin: m, padding: p) {
@each $size, $lengths in $spacers {
$length-x: map.get($lengths, x);
$length-y: map.get($lengths, y);
.#{$abbrev}-a-#{$size} {
#{$prop}: $length-y $length-x !important;
} // a = All sides
.#{$abbrev}-t-#{$size} {
#{$prop}-top: $length-y !important;
}
.#{$abbrev}-r-#{$size} {
#{$prop}-right: $length-x !important;
}
.#{$abbrev}-b-#{$size} {
#{$prop}-bottom: $length-y !important;
}
.#{$abbrev}-l-#{$size} {
#{$prop}-left: $length-x !important;
}
// Axes
.#{$abbrev}-x-#{$size} {
#{$prop}-right: $length-x !important;
#{$prop}-left: $length-x !important;
}
.#{$abbrev}-y-#{$size} {
#{$prop}-top: $length-y !important;
#{$prop}-bottom: $length-y !important;
}
}
}

@ -13,12 +13,10 @@
@import 'base/type'; @import 'base/type';
@import 'base/forms'; @import 'base/forms';
@import 'base/grid'; @import 'base/grid';
@import 'base/fonts'; @import 'base/font_awesome';
@import 'base/code';
// UTILS // UTILS
@import 'utils/utils'; @import 'utils/utils';
@import 'utils/spacings';
@import 'utils/widths'; @import 'utils/widths';
// COMPONENTS // COMPONENTS
@ -33,7 +31,6 @@
@import 'components/dropdown'; @import 'components/dropdown';
@import 'components/infobox'; @import 'components/infobox';
@import 'components/query_editor'; @import 'components/query_editor';
@import 'components/tabbed_view';
@import 'components/query_part'; @import 'components/query_part';
@import 'components/json_explorer'; @import 'components/json_explorer';
@import 'components/dashboard_grid'; @import 'components/dashboard_grid';

@ -1,62 +0,0 @@
//
// Code (inline and blocK)
// --------------------------------------------------
// Inline and block code styles
code,
pre {
@include font-family-monospace();
font-size: $font-size-base - 2;
background-color: $code-tag-bg;
color: $text-color;
border: 1px solid $code-tag-border;
border-radius: 4px;
}
// Inline code
code {
color: $text-color;
white-space: nowrap;
padding: 2px 5px;
margin: 0 2px;
}
code.code--small {
font-size: $font-size-xs;
padding: $space-xxs;
margin: 0 2px;
}
// Blocks of code
pre {
display: block;
margin: 0 0 $line-height-base;
line-height: $line-height-base;
word-break: break-all;
word-wrap: break-word;
white-space: pre;
white-space: pre-wrap;
background-color: $code-tag-bg;
padding: 10px;
&.pre--no-style {
background: transparent;
border: none;
padding: 0px;
}
// Make prettyprint styles more spaced out for readability
&.prettyprint {
margin-bottom: $line-height-base;
}
// Account for some code outputs that place code tags in pre tags
code {
padding: 0;
color: inherit;
white-space: pre;
white-space: pre-wrap;
background-color: transparent;
border: 0;
}
}

@ -1,49 +0,0 @@
@import 'font_awesome';
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(#{$font-file-path}/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin */
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(#{$font-file-path}/roboto/L0xTDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vrtSM1J-gEPT5Ese6hmHSh0mQ.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/*
To add new variations/version of Inter, download from https://rsms.me/inter/ and add the
web font files to the public/fonts/inter folder. Do not download the fonts from Google Fonts
or somewhere else because they don't support the features we require (like tabular numerals).
If adding additional weights, consider switching to the InterVariable variable font as combined
it may take less space than multiple static weights.
*/
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('#{$font-file-path}/inter/Inter-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('#{$font-file-path}/inter/Inter-Medium.woff2') format('woff2');
}

@ -1,4 +0,0 @@
.section-heading {
font-size: $font-size-md;
margin-bottom: $space-sm;
}

@ -1,50 +0,0 @@
@use 'sass:map';
// Margin and Padding
.m-x-auto {
margin-right: auto !important;
margin-left: auto !important;
}
@each $prop, $abbrev in (margin: m, padding: p) {
@each $size, $lengths in $spacers {
$length-x: map.get($lengths, x);
$length-y: map.get($lengths, y);
.#{$abbrev}-a-#{$size} {
#{$prop}: $length-y $length-x !important;
} // a = All sides
.#{$abbrev}-t-#{$size} {
#{$prop}-top: $length-y !important;
}
.#{$abbrev}-r-#{$size} {
#{$prop}-right: $length-x !important;
}
.#{$abbrev}-b-#{$size} {
#{$prop}-bottom: $length-y !important;
}
.#{$abbrev}-l-#{$size} {
#{$prop}-left: $length-x !important;
}
// Axes
.#{$abbrev}-x-#{$size} {
#{$prop}-right: $length-x !important;
#{$prop}-left: $length-x !important;
}
.#{$abbrev}-y-#{$size} {
#{$prop}-top: $length-y !important;
#{$prop}-bottom: $length-y !important;
}
}
}
// Positioning
.pos-f-t {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: $zindex-navbar-fixed;
}

@ -42,6 +42,7 @@ load(
) )
ver_mode = "release" ver_mode = "release"
semver_regex = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
def retrieve_npm_packages_step(): def retrieve_npm_packages_step():
return { return {
@ -59,6 +60,34 @@ def retrieve_npm_packages_step():
"commands": ["./bin/build artifacts npm retrieve --tag ${DRONE_TAG}"], "commands": ["./bin/build artifacts npm retrieve --tag ${DRONE_TAG}"],
} }
def release_pr_step(depends_on = []):
return {
"name": "create-release-pr",
"image": images["curl"],
"depends_on": depends_on,
"environment": {
"GITHUB_TOKEN": from_secret("github_token"),
"GH_CLI_URL": "https://github.com/cli/cli/releases/download/v2.50.0/gh_2.50.0_linux_amd64.tar.gz",
},
"commands": [
"apk add perl",
"v_target=`echo $${{TAG}} | perl -pe 's/{}/v\\1.\\2.x/'`".format(semver_regex),
"default_target=`if [[ -n $$LATEST ]]; then echo 'main'; else echo $$v_target; fi`",
"backport=`if [[ -n $$LATEST ]]; then echo $$v_target; fi`",
# Install gh CLI
"curl -L $${GH_CLI_URL} | tar -xz --strip-components=1 -C /usr",
# Run the release-pr workflow
"gh workflow run " +
"-f dry_run=$${DRY_RUN} " +
"-f version=$${TAG} " +
# If the submitter has set a target branch, then use that, otherwise use the default
"-f target=$${TARGET:-$default_target} " +
# If the submitter has set a backport branch, then use that, otherwise use the default
"-f backport=$${BACKPORT:-$default_backport} " +
"--repo=grafana/grafana release-pr.yml",
],
}
def release_npm_packages_step(): def release_npm_packages_step():
return { return {
"name": "release-npm-packages", "name": "release-npm-packages",
@ -136,9 +165,20 @@ def publish_artifacts_pipelines(mode):
publish_artifacts_step(), publish_artifacts_step(),
publish_static_assets_step(), publish_static_assets_step(),
publish_storybook_step(), publish_storybook_step(),
release_pr_step(depends_on = ["publish-artifacts", "publish-static-assets"]),
] ]
return [ return [
pipeline(
name = "create-release-pr",
trigger = {
"event": ["promote"],
"target": "release-pr",
},
steps = [
release_pr_step(),
],
),
pipeline( pipeline(
name = "publish-artifacts-{}".format(mode), name = "publish-artifacts-{}".format(mode),
trigger = trigger, trigger = trigger,

Loading…
Cancel
Save