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. 58
      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. 4
      pkg/infra/tracing/tracing.go
  12. 2
      pkg/infra/tracing/tracing_config.go
  13. 1
      pkg/services/apiserver/standalone/options/tracing.go
  14. 11
      pkg/services/licensing/oss.go
  15. 135
      pkg/services/navtree/models.go
  16. 180
      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. 131
      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"]
],
"public/app/features/datasources/components/DataSourceTestingStatus.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[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 />", "3"],
[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"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
],
"public/app/features/datasources/components/DataSourceTypeCard.tsx:5381": [
[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, "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": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[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, "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": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],

@ -2731,6 +2731,47 @@ volumes:
clone:
retries: 3
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:
EDITION: oss
image_pull_secrets:
@ -2787,6 +2828,24 @@ steps:
from_secret: prerelease_bucket
image: grafana/grafana-ci-deploy:1.3.3
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:
event:
- promote
@ -4893,6 +4952,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 08f38b820f97302de03a9fdfd39fb12c185bb36170704cf7591c16f33c3e4d31
hmac: 043028c50d984e1ea98a294c6746df1388cb0b7d7976f82f3dd0004fc493bafc
...

@ -57,6 +57,6 @@ jobs:
- name: Create PR with backports
if: "${{ github.event.inputs.backport != '' }}"
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:
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)
- [Azure CosmosDB](/grafana/plugins/grafana-azurecosmosdb-datasource)
- [Azure Devops](/grafana/plugins/grafana-azuredevops-datasource)
- [Catchpoint](/grafana/plugins/grafana-catchpoint-datasource)
- [Databricks](/grafana/plugins/grafana-databricks-datasource)
- [DataDog](/grafana/plugins/grafana-datadog-datasource)
- [Dynatrace](/grafana/plugins/grafana-dynatrace-datasource)

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

@ -1,10 +1,10 @@
import { css, cx } from '@emotion/css';
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 { useTheme2 } from '../../themes';
import { useStyles2, useTheme2 } from '../../themes';
import { FormField } from '../FormField/FormField';
import { InlineFormLabel } from '../FormLabel/FormLabel';
import { InlineField } from '../Forms/InlineField';
@ -37,28 +37,38 @@ const DEFAULT_ACCESS_OPTION = {
value: 'proxy',
};
const HttpAccessHelp = () => (
<div className="grafana-info-box m-t-2">
<p>
Access mode controls how requests to the data source will be handled.
<strong>
&nbsp;<i>Server</i>
</strong>{' '}
should be the preferred way if nothing else is stated.
</p>
<div className="alert-title">Server access mode (Default):</div>
<p>
All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to
the data source and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements. The URL needs
to be accessible from the grafana backend/server if you select this access mode.
</p>
<div className="alert-title">Browser access mode:</div>
<p>
All requests will be made from the browser directly to the data source and may be subject to Cross-Origin Resource
Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access mode.
</p>
</div>
);
const HttpAccessHelp = () => {
const styles = useStyles2(getAccessStyles);
return (
<div className={cx('grafana-info-box', styles.infoBox)}>
<p>
Access mode controls how requests to the data source will be handled.
<strong>
&nbsp;<i>Server</i>
</strong>{' '}
should be the preferred way if nothing else is stated.
</p>
<div className="alert-title">Server access mode (Default):</div>
<p>
All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to
the data source and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements. The URL needs
to be accessible from the grafana backend/server if you select this access mode.
</p>
<div className="alert-title">Browser access mode:</div>
<p>
All requests will be made from the browser directly to the data source and may be subject to Cross-Origin
Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access
mode.
</p>
</div>
);
};
const getAccessStyles = (theme: GrafanaTheme2) => ({
infoBox: css({
marginTop: theme.spacing(3),
}),
});
const LABEL_WIDTH = 26;

@ -5,8 +5,10 @@ import { useTheme2 } from '../ThemeContext';
import { getAgularPanelStyles } from './angularPanelStyles';
import { getCardStyles } from './card';
import { getCodeStyles } from './code';
import { getElementStyles } from './elements';
import { getExtraStyles } from './extra';
import { getFontStyles } from './fonts';
import { getFormElementStyles } from './forms';
import { getLegacySelectStyles } from './legacySelect';
import { getMarkdownStyles } from './markdownStyles';
@ -22,8 +24,10 @@ export function GlobalStyles() {
return (
<Global
styles={[
getCodeStyles(theme),
getElementStyles(theme),
getExtraStyles(theme),
getFontStyles(theme),
getFormElementStyles(theme),
getPageStyles(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)
data.NavTree.ApplyAdminIA()
data.NavTree.ApplyCostManagementIA()
data.NavTree.ApplyHelpVersion(data.Settings.BuildInfo.VersionString) // RunIndexDataHooks can modify the version string
data.NavTree.Sort()

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

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

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

@ -59,12 +59,13 @@ func ProvideService(cfg *setting.Cfg, hooksService *hooks.HooksService) *OSSLice
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{
Text: "Stats and license",
Id: "upgrading",
Url: l.LicenseURL(req.IsGrafanaAdmin),
Icon: "unlock",
Text: "Stats and license",
Id: "upgrading",
Url: l.LicenseURL(req.IsGrafanaAdmin),
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)
var costManagementApp *NavLink
var adaptiveMetricsApp *NavLink
var attributionsApp *NavLink
var logVolumeExplorerApp *NavLink
if orgAdminNode != nil {
adminNodeLinks := []*NavLink{}
generalNodeLinks := []*NavLink{}
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("upgrading")) // TODO does this even exist
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("licensing"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("org-settings"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("server-settings"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("global-orgs"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("feature-toggles"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("storage"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("migrate-to-cloud"))
generalNodeLinks = AppendIfNotNil(generalNodeLinks, root.FindById("banner-settings"))
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{}
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("global-users"))
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("teams"))
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("standalone-plugin-page-/a/grafana-auth-app"))
accessNodeLinks = AppendIfNotNil(accessNodeLinks, root.FindById("serviceaccounts"))
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 len(generalNode.Children) > 0 {
adminNodeLinks = append(adminNodeLinks, generalNode)
}
if len(pluginsNode.Children) > 0 {
adminNodeLinks = append(adminNodeLinks, pluginsNode)
}
if len(usersNode.Children) > 0 {
adminNodeLinks = append(adminNodeLinks, usersNode)
}
authenticationNode := root.FindById("authentication")
if authenticationNode != nil {
authenticationNode.IsSection = true
adminNodeLinks = append(adminNodeLinks, authenticationNode)
}
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)
for _, element := range orgAdminNode.Children {
switch navId := element.Id; navId {
case "plugin-page-grafana-costmanagementui-app":
costManagementApp = element
case "plugin-page-grafana-adaptive-metrics-app":
adaptiveMetricsApp = element
case "plugin-page-grafana-attributions-app":
attributionsApp = element
case "plugin-page-grafana-logvolumeexplorer-app":
logVolumeExplorerApp = element
default:
adminNodeLinks = append(adminNodeLinks, element)
}
}
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 costManagementApp != nil {
costManagementMetricsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/metrics")
if costManagementMetricsNode != nil {
if adaptiveMetricsApp != nil {
costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, adaptiveMetricsApp)
}
if attributionsApp != nil {
costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, attributionsApp)
}
}
if len(adminNodeLinks) > 0 {
orgAdminNode.Children = adminNodeLinks
} else {
root.RemoveSection(orgAdminNode)
costManagementLogsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/logs")
if costManagementLogsNode != nil {
if logVolumeExplorerApp != nil {
costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerApp)
}
}
adminNodeLinks = append(adminNodeLinks, costManagementApp)
}
orgAdminNode.Children = adminNodeLinks
}
}

@ -21,10 +21,71 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
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
// 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) {
configNodes = append(configNodes, &navtree.NavLink{
pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
Text: "Plugins",
Id: "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",
})
}
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))) {
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",
})
}
if hasAccess(ac.TeamsAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{
accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "Teams",
Id: "teams",
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",
})
}
if enableServiceAccount(s, c) {
configNodes = append(configNodes, &navtree.NavLink{
accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "Service accounts",
Id: "serviceaccounts",
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",
})
}
disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID())
if err != nil {
return nil, err
}
if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled {
configNodes = append(configNodes, &navtree.NavLink{
accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "API keys",
Id: "apikeys",
SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs",
@ -73,80 +154,31 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
if hasAccess(ac.OrgPreferencesAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Default preferences",
Id: "org-settings",
SubTitle: "Manage preferences across an organization",
Icon: "sliders-v-alt",
Url: s.cfg.AppSubURL + "/org",
})
}
if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) ||
(hasAccess(ssoutils.OauthSettingsEvaluator(s.cfg)) && s.features.IsEnabled(ctx, featuremgmt.FlagSsoSettingsApi)) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Authentication",
Id: "authentication",
SubTitle: "Manage your auth settings and configure single sign-on",
Icon: "signin",
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",
})
usersNode := &navtree.NavLink{
Text: "Users and access",
SubTitle: "Configure access for individual users, teams, and service accounts",
Id: navtree.NavIDCfgAccess,
Url: "/admin/access",
Icon: "shield",
Children: accessNodeLinks,
}
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 len(usersNode.Children) > 0 {
configNodes = append(configNodes, usersNode)
}
if s.features.IsEnabled(ctx, featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) {
if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) ||
(hasAccess(ssoutils.OauthSettingsEvaluator(s.cfg)) && s.features.IsEnabled(ctx, featuremgmt.FlagSsoSettingsApi)) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Correlations",
Icon: "gf-glue",
SubTitle: "Add and configure correlations",
Id: "correlations",
Url: s.cfg.AppSubURL + "/datasources/correlations",
Text: "Authentication",
Id: "authentication",
SubTitle: "Manage your auth settings and configure single sign-on",
Icon: "signin",
IsSection: true,
Url: s.cfg.AppSubURL + "/admin/authentication",
})
}
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{
Id: navtree.NavIDCfg,
Text: "Administration",

@ -295,7 +295,7 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"},
"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-adaptive-metrics-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Metrics"},
"grafana-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"},
@ -307,7 +307,7 @@ func (s *ServiceImpl) readNavigationSettings() {
}
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")

@ -151,7 +151,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere
orgAdminNode, err := s.getAdminNode(c)
if orgAdminNode != nil {
if orgAdminNode != nil && len(orgAdminNode.Children) > 0 {
treeRoot.AddSection(orgAdminNode)
} else if err != nil {
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"
"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/sqlstash/sqltemplate"
)
@ -33,7 +32,7 @@ func getCurrentUser(ctx context.Context) (string, error) {
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.

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

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

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

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

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

@ -4,7 +4,7 @@ import React, { HTMLAttributes } from 'react';
import { DataSourceSettings as DataSourceSettingsType, GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
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 { trackCreateDashboardClicked } from '../tracking';
@ -25,16 +25,16 @@ interface AlertMessageProps extends HTMLAttributes<HTMLDivElement> {
const getStyles = (theme: GrafanaTheme2, hasTitle: boolean) => {
return {
content: css`
color: ${theme.colors.text.secondary};
padding-top: ${hasTitle ? theme.spacing(1) : 0};
max-height: 50vh;
overflow-y: auto;
`,
disabled: css`
pointer-events: none;
color: ${theme.colors.text.secondary};
`,
content: css({
color: theme.colors.text.secondary,
paddingTop: hasTitle ? theme.spacing(1) : 0,
maxHeight: '50vh',
overflowY: 'auto',
}),
disabled: css({
pointerEvents: 'none',
color: theme.colors.text.secondary,
}),
};
};
@ -95,10 +95,11 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource
path: location.pathname,
});
};
const styles = useStyles2(getTestingStatusStyles);
if (message) {
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}>
{testingStatus?.details && (
<>
@ -123,3 +124,9 @@ export function DataSourceTestingStatus({ testingStatus, exploreUrl, dataSource
return null;
}
const getTestingStatusStyles = (theme: GrafanaTheme2) => ({
container: css({
paddingTop: theme.spacing(3),
}),
});

@ -53,7 +53,7 @@ describe('buildCategories', () => {
it('should add enterprise phantom plugins', () => {
const enterprisePluginsCategory = categories[3];
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[enterprisePluginsCategory.plugins.length - 1].name).toBe('Wavefront');
});

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

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

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

@ -228,7 +228,7 @@ export class QueryInspector extends PureComponent<Props, State> {
return (
<div className={styles.wrap}>
<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">
<Trans i18nKey="inspector.query.description">
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) => {
return {
wrap: css`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
flex: 1 1 0;
min-height: 0;
`,
toolbar: css`
display: flex;
width: 100%;
flex-grow: 0;
align-items: center;
justify-content: flex-end;
margin-bottom: ${theme.v1.spacing.sm};
`,
toolbarItem: css`
margin-left: ${theme.v1.spacing.md};
`,
content: css`
flex-grow: 1;
height: 100%;
`,
editor: css`
font-family: monospace;
height: 100%;
flex-grow: 1;
`,
viewer: css`
overflow: scroll;
`,
dataFrameSelect: css`
flex-grow: 2;
`,
leftActions: css`
display: flex;
flex-grow: 1;
heading: css({
fontSize: theme.typography.body.fontSize,
marginBottom: theme.spacing(1),
}),
wrap: css({
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
flex: '1 1 0',
minHeight: 0,
}),
toolbar: css({
display: 'flex',
width: '100%',
flexGrow: 0,
alignItems: 'center',
justifyContent: 'flex-end',
marginBottom: theme.v1.spacing.sm,
}),
toolbarItem: css({
marginLeft: theme.v1.spacing.md,
}),
content: css({
flexGrow: 1,
height: '100%',
}),
editor: css({
fontFamily: 'monospace',
height: '100%',
flexGrow: 1,
}),
viewer: css({
overflow: 'scroll',
}),
dataFrameSelect: css({
flexGrow: 2,
}),
leftActions: css({
display: 'flex',
flexGrow: 1,
max-width: 85%;
@media (max-width: 1345px) {
max-width: 75%;
}
`,
options: css`
padding-top: ${theme.v1.spacing.sm};
`,
dataDisplayOptions: css`
flex-grow: 1;
min-width: 300px;
margin-right: ${theme.v1.spacing.sm};
`,
selects: css`
display: flex;
> * {
margin-right: ${theme.v1.spacing.sm};
}
`,
maxWidth: '85%',
'@media (max-width: 1345px)': {
maxWidth: '75%',
},
}),
options: css({
paddingTop: theme.v1.spacing.sm,
}),
dataDisplayOptions: css({
flexGrow: 1,
minWidth: '300px',
marginRight: theme.v1.spacing.sm,
}),
selects: css({
display: 'flex',
'> *': {
marginRight: theme.v1.spacing.sm,
},
}),
};
};

@ -1,8 +1,10 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
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 { Page } from 'app/core/components/Page/Page';
import { getConfig } from 'app/core/config';
@ -37,6 +39,7 @@ export const SignupInvitedPage = ({ match }: Props) => {
const [initFormModel, setInitFormModel] = useState<FormModel>();
const [greeting, setGreeting] = useState<string>();
const [invitedBy, setInvitedBy] = useState<string>();
const styles = useStyles2(getStyles);
useAsync(async () => {
const invite = await getBackendSrv().get(`/api/user/invite/${code}`);
@ -65,7 +68,7 @@ export const SignupInvitedPage = ({ match }: Props) => {
<Page.Contents>
<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{' '}
<span className="highlight-word">{contextSrv.user.orgName}</span>
<br />
@ -109,4 +112,10 @@ export const SignupInvitedPage = ({ match }: Props) => {
);
};
const getStyles = (theme: GrafanaTheme2) => ({
tagline: css({
paddingBottom: theme.spacing(3),
}),
});
export default SignupInvitedPage;

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

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

@ -1,7 +1,8 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { Box, InlineField, Input, TagsInput } from '@grafana/ui';
import { GrafanaTheme2, QueryEditorProps } from '@grafana/data';
import { Box, InlineField, Input, TagsInput, useStyles2 } from '@grafana/ui';
import { GraphiteDatasource } from '../datasource';
import { GraphiteQuery, GraphiteOptions } from '../types';
@ -32,6 +33,7 @@ export const AnnotationEditor = (props: QueryEditorProps<GraphiteDatasource, Gra
setTags(tagsInput);
updateValue('tags', tagsInput);
};
const styles = useStyles2(getStyles);
return (
<Box marginBottom={5}>
@ -44,7 +46,7 @@ export const AnnotationEditor = (props: QueryEditorProps<GraphiteDatasource, Gra
/>
</InlineField>
<h5 className="section-heading">Or</h5>
<h5 className={styles.heading}>Or</h5>
<InlineField label="Graphite events tags" labelWidth={24}>
<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>
);
};
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
// once angular is disabled, this file can be deleted
@use 'sass:map';
.edit-tab-content {
flex-grow: 1;
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/forms';
@import 'base/grid';
@import 'base/fonts';
@import 'base/code';
@import 'base/font_awesome';
// UTILS
@import 'utils/utils';
@import 'utils/spacings';
@import 'utils/widths';
// COMPONENTS
@ -33,7 +31,6 @@
@import 'components/dropdown';
@import 'components/infobox';
@import 'components/query_editor';
@import 'components/tabbed_view';
@import 'components/query_part';
@import 'components/json_explorer';
@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"
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():
return {
@ -59,6 +60,34 @@ def retrieve_npm_packages_step():
"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():
return {
"name": "release-npm-packages",
@ -136,9 +165,20 @@ def publish_artifacts_pipelines(mode):
publish_artifacts_step(),
publish_static_assets_step(),
publish_storybook_step(),
release_pr_step(depends_on = ["publish-artifacts", "publish-static-assets"]),
]
return [
pipeline(
name = "create-release-pr",
trigger = {
"event": ["promote"],
"target": "release-pr",
},
steps = [
release_pr_step(),
],
),
pipeline(
name = "publish-artifacts-{}".format(mode),
trigger = trigger,

Loading…
Cancel
Save