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

This commit is contained in:
Ryan McKinley
2024-06-12 22:02:53 +03:00
45 changed files with 2428 additions and 627 deletions
+3 -26
View File
@@ -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"]
],
+60 -1
View File
@@ -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
...
+1 -1
View File
@@ -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')",
},
},
]);
}
+1 -1
View File
@@ -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()
+3 -1
View File
@@ -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
+2
View File
@@ -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 {
+6 -5
View File
@@ -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,
})
}
})
+36 -99
View File
@@ -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,
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)
}
}
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"))
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)
}
}
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)
}
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
} 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
}
}
+120 -88
View File
@@ -21,60 +21,9 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
authConfigUIAvailable := s.license.FeatureEnabled(social.SAMLProviderName) || s.cfg.LDAPAuthEnabled
// 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{
Text: "Plugins",
Id: "plugins",
SubTitle: "Extend the Grafana experience with plugins",
Icon: "plug",
Url: s.cfg.AppSubURL + "/plugins",
})
}
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll))) {
configNodes = append(configNodes, &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{
Text: "Teams",
Id: "teams",
SubTitle: "Groups of users that have common dashboard and permission needs",
Icon: "users-alt",
Url: s.cfg.AppSubURL + "/org/teams",
})
}
if enableServiceAccount(s, c) {
configNodes = append(configNodes, &navtree.NavLink{
Text: "Service accounts",
Id: "serviceaccounts",
SubTitle: "Use service accounts to run automated workloads in Grafana",
Icon: "gf-service-account",
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{
Text: "API keys",
Id: "apikeys",
SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs",
Icon: "key-skeleton-alt",
Url: s.cfg.AppSubURL + "/org/apikeys",
})
}
generalNodeLinks := []*navtree.NavLink{}
if hasAccess(ac.OrgPreferencesAccessEvaluator) {
configNodes = append(configNodes, &navtree.NavLink{
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Default preferences",
Id: "org-settings",
SubTitle: "Manage preferences across an organization",
@@ -82,32 +31,18 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
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{
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) {
configNodes = append(configNodes, &navtree.NavLink{
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)) {
configNodes = append(configNodes, &navtree.NavLink{
generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Feature Toggles",
SubTitle: "View and edit feature toggles",
Id: "feature-toggles",
@@ -115,9 +50,51 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
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) {
pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
Text: "Plugins",
Id: "plugins",
SubTitle: "Extend the Grafana experience with plugins",
Icon: "plug",
Url: s.cfg.AppSubURL + "/plugins",
})
}
if s.features.IsEnabled(ctx, featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) {
configNodes = append(configNodes, &navtree.NavLink{
pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
Text: "Correlations",
Icon: "gf-glue",
SubTitle: "Add and configure correlations",
@@ -126,25 +103,80 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
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)
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 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)
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))) {
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) {
accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "Teams",
Id: "teams",
SubTitle: "Groups of users that have common dashboard and permission needs",
Icon: "users-alt",
Url: s.cfg.AppSubURL + "/org/teams",
})
}
if enableServiceAccount(s, c) {
accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "Service accounts",
Id: "serviceaccounts",
SubTitle: "Use service accounts to run automated workloads in Grafana",
Icon: "gf-service-account",
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 {
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",
Icon: "key-skeleton-alt",
Url: s.cfg.AppSubURL + "/org/apikeys",
})
}
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 len(usersNode.Children) > 0 {
configNodes = append(configNodes, usersNode)
}
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",
IsSection: true,
Url: s.cfg.AppSubURL + "/admin/authentication",
})
}
configNode := &navtree.NavLink{
+2 -2
View File
@@ -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")
+1 -1
View File
@@ -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
-30
View File
@@ -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)
}
+1 -2
View File
@@ -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!);
{isNodeLoading && <Skeleton count={5} className={styles.loader} />}
if (anyChildExpanded && !childNode.isExpanded && !isSelected) {
return null;
}
{!isNodeLoading &&
childNodesArr.map((childNode) => {
const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
const childNodePath = [...nodePath, childNode.name];
if (anyChildExpanded && !childNode.isExpanded && !isSelected) {
return null;
}
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}
const childNodePath = [...nodePath, childNode.name];
{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);
}}
/>
)}
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}
<span data-testid={`scopes-tree-${childNode.name}-title`}>{childNode.title}</span>
{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 className={styles.itemChildren}>
{childNode.isExpanded && (
<ScopesTreeLevel
showQuery={showQuery}
nodes={node.nodes}
nodePath={childNodePath}
loadingNodeName={loadingNodeName}
scopeNames={scopeNames}
onNodeUpdate={onNodeUpdate}
onNodeSelectToggle={onNodeSelectToggle}
/>
)}
</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
+60 -56
View File
@@ -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,
},
}),
};
};
+11 -2
View File
@@ -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

+40
View File
@@ -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;
}
}
}
+1 -4
View File
@@ -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';
-62
View File
@@ -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;
}
}
-49
View File
@@ -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');
}
-4
View File
@@ -1,4 +0,0 @@
.section-heading {
font-size: $font-size-md;
margin-bottom: $space-sm;
}
-50
View File
@@ -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;
}
+40
View File
@@ -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,