From c043a2f83abc2486b7357f4312e6f4e57bae0169 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Tue, 24 May 2022 14:58:34 +0100 Subject: [PATCH 01/15] Chore: Update theme json (#49480) --- public/sass/theme.dark.generated.json | 4 ++++ public/sass/theme.light.generated.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/public/sass/theme.dark.generated.json b/public/sass/theme.dark.generated.json index 0952f01c4a5..178b38b2614 100644 --- a/public/sass/theme.dark.generated.json +++ b/public/sass/theme.dark.generated.json @@ -152,6 +152,10 @@ "menuTabs": { "height": 41 }, + "textHighlight": { + "text": "#000000", + "background": "#F5B73D" + }, "horizontalDrawer": { "defaultHeight": 400 } diff --git a/public/sass/theme.light.generated.json b/public/sass/theme.light.generated.json index 9e1b08ec313..ddd149cd45c 100644 --- a/public/sass/theme.light.generated.json +++ b/public/sass/theme.light.generated.json @@ -152,6 +152,10 @@ "menuTabs": { "height": 41 }, + "textHighlight": { + "text": "#000000", + "background": "#FAD34A" + }, "horizontalDrawer": { "defaultHeight": 400 } From 20f8b8625ac0f3f61e8fa33df8552813ea8335c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 24 May 2022 15:58:44 +0200 Subject: [PATCH 02/15] Search: Add primary background for global search container (#49473) * Search: Add primarhy background for global search container * Fix spacing for ActionRow * Fixing ts issue --- .../features/search/components/ActionRow.tsx | 17 ++++++++--------- .../search/components/DashboardSearch.tsx | 7 +++++-- .../search/page/components/ActionRow.tsx | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/public/app/features/search/components/ActionRow.tsx b/public/app/features/search/components/ActionRow.tsx index ef9c5d13c65..12b5c0a67cd 100644 --- a/public/app/features/search/components/ActionRow.tsx +++ b/public/app/features/search/components/ActionRow.tsx @@ -1,9 +1,9 @@ import { css } from '@emotion/css'; import React, { FC, ChangeEvent, FormEvent } from 'react'; -import { GrafanaTheme, SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { HorizontalGroup, RadioButtonGroup, stylesFactory, useTheme, Checkbox, InlineSwitch } from '@grafana/ui'; +import { HorizontalGroup, RadioButtonGroup, Checkbox, InlineSwitch, useStyles2 } from '@grafana/ui'; import { SortPicker } from 'app/core/components/Select/SortPicker'; import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; import { SearchSrv } from 'app/core/services/search_srv'; @@ -40,8 +40,7 @@ export const ActionRow: FC = ({ hideLayout, showPreviews, }) => { - const theme = useTheme(); - const styles = getStyles(theme); + const styles = useStyles2(getStyles); const previewsEnabled = config.featureToggles.dashboardPreviews; return ( @@ -78,21 +77,21 @@ export const ActionRow: FC = ({ ActionRow.displayName = 'ActionRow'; -const getStyles = stylesFactory((theme: GrafanaTheme) => { +export const getStyles = (theme: GrafanaTheme2) => { return { actionRow: css` display: none; - @media only screen and (min-width: ${theme.breakpoints.md}) { + ${theme.breakpoints.up('md')} { display: flex; justify-content: space-between; align-items: center; - padding: ${theme.spacing.lg} 0; + padding-bottom: ${theme.spacing(2)}; width: 100%; } `, rowContainer: css` - margin-right: ${theme.spacing.md}; + margin-right: ${theme.spacing(1)}; `, checkboxWrapper: css` label { @@ -100,4 +99,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { } `, }; -}); +}; diff --git a/public/app/features/search/components/DashboardSearch.tsx b/public/app/features/search/components/DashboardSearch.tsx index 33f7ff2f668..065ce775f38 100644 --- a/public/app/features/search/components/DashboardSearch.tsx +++ b/public/app/features/search/components/DashboardSearch.tsx @@ -140,11 +140,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { max-width: 1400px; margin: 0 auto; padding: ${theme.spacing(2)}; + background: ${theme.colors.background.primary}; + border: 1px solid ${theme.components.panel.borderColor}; + margin-top: ${theme.spacing(4)}; height: 100%; ${theme.breakpoints.up('md')} { - padding: ${theme.spacing(4)}; + padding: ${theme.spacing(3)}; } `, closeBtn: css` @@ -160,7 +163,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => { display: flex; flex-direction: column; height: 100%; - padding-bottom: ${theme.spacing(3)}; + padding: ${theme.spacing(2, 0, 3, 0)}; `, input: css` box-sizing: border-box; diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index c6a86240f05..7c309c11cff 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -104,11 +104,11 @@ export const getStyles = (theme: GrafanaTheme2) => { actionRow: css` display: none; - @media only screen and (min-width: ${theme.v1.breakpoints.md}) { + ${theme.breakpoints.up('md')} { display: flex; justify-content: space-between; align-items: center; - padding: ${theme.v1.spacing.lg} 0; + padding-bottom: ${theme.spacing(2)}; width: 100%; } `, From 3408677547a9375cee9d23ae291b6d523de63430 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 24 May 2022 10:09:39 -0400 Subject: [PATCH 03/15] ReleaseNotes: Updated changelog and release notes for 9.0.0-beta1 (#49489) --- CHANGELOG.md | 33 +++++++++++++++++ docs/sources/release-notes/_index.md | 1 + .../release-notes-9-0-0-beta1.md | 36 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-9-0-0-beta1.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bf240475032..9eb26796bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ + + +# 9.0.0-beta1 (2022-05-24) + +### Features and enhancements + +- **AccessControl:** Add setting for permission cache. (Enterprise) +- **AccessControl:** Check dashboard permissions for reports. (Enterprise) +- **Auth:** Remove grafana ui dependency to the aws sdk. [#43559](https://github.com/grafana/grafana/pull/43559), [@sunker](https://github.com/sunker) +- **BasicRoles:** Add API endpoint to reset basic roles permissions to factory. (Enterprise) +- **LDAP Mapping:** Allow Grafana Admin mapping without org role. [#37189](https://github.com/grafana/grafana/pull/37189), [@krzysdabro](https://github.com/krzysdabro) +- **Licensing:** Only enforce total number of users. (Enterprise) +- **Loki:** do not convert NaN to null. [#45389](https://github.com/grafana/grafana/pull/45389), [@gabor](https://github.com/gabor) +- **Report:** API support for multiple dashboards. (Enterprise) +- **Report:** Support sending embedded image in the report email. (Enterprise) +- **Report:** UI for multiple dashboards. (Enterprise) +- **Reporting:** Remove redundant empty attachment when export to CSV is enabled. (Enterprise) +- **SAML:** Implement Name Templates for assertion_attribute_name option. (Enterprise) +- **SSE/Alerting:** Support prom instant vector responses. [#44865](https://github.com/grafana/grafana/pull/44865), [@kylebrandt](https://github.com/kylebrandt) +- **Tracing:** Add trace to metrics config behind feature toggle. [#46298](https://github.com/grafana/grafana/pull/46298), [@connorlindsey](https://github.com/connorlindsey) + +### Bug fixes + +- **Fix:** Prevent automatic parsing of string data types to numbers. [#46035](https://github.com/grafana/grafana/pull/46035), [@joshhunt](https://github.com/joshhunt) +- **Prometheus:** Fix inconsistent labels in exemplars resulting in marshal json error. [#46135](https://github.com/grafana/grafana/pull/46135), [@hanjm](https://github.com/hanjm) + +### Breaking changes + +In the Loki data source, for consistency and performance reasons, we changed how we represent `NaN` (not a number) values received from Loki. In the past versions, we converted these to `null` in the frontend (for dashboard and explore), and kept as `NaN` in the alerting path. Starting with this version, we will always keep it as `NaN`. This change should be mostly invisible for the users. Issue [#45389](https://github.com/grafana/grafana/issues/45389) + +The dependency to [grafana/aws-sdk](https://github.com/grafana/grafana-aws-sdk-react) is moved from [grafana/ui](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/package.json) to the plugin. This means that any plugin that use SIGV4 auth need to pass a SIGV4 editor component as a prop to the `DataSourceHttpSettings` component. Issue [#43559](https://github.com/grafana/grafana/issues/43559) + + # 8.5.3 diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index b773f705a1c..d9890a61da9 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -9,6 +9,7 @@ weight = 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 9.0.0-beta1]({{< relref "release-notes-9-0-0-beta1" >}}) - [Release notes for 8.5.3]({{< relref "release-notes-8-5-3" >}}) - [Release notes for 8.5.2]({{< relref "release-notes-8-5-2" >}}) - [Release notes for 8.5.1]({{< relref "release-notes-8-5-1" >}}) diff --git a/docs/sources/release-notes/release-notes-9-0-0-beta1.md b/docs/sources/release-notes/release-notes-9-0-0-beta1.md new file mode 100644 index 00000000000..ccf304ee60a --- /dev/null +++ b/docs/sources/release-notes/release-notes-9-0-0-beta1.md @@ -0,0 +1,36 @@ ++++ +title = "Release notes for Grafana 9.0.0-beta1" +hide_menu = true ++++ + + + +# Release notes for Grafana 9.0.0-beta1 + +### Features and enhancements + +- **AccessControl:** Add setting for permission cache. (Enterprise) +- **AccessControl:** Check dashboard permissions for reports. (Enterprise) +- **Auth:** Remove grafana ui dependency to the aws sdk. [#43559](https://github.com/grafana/grafana/pull/43559), [@sunker](https://github.com/sunker) +- **BasicRoles:** Add API endpoint to reset basic roles permissions to factory. (Enterprise) +- **LDAP Mapping:** Allow Grafana Admin mapping without org role. [#37189](https://github.com/grafana/grafana/pull/37189), [@krzysdabro](https://github.com/krzysdabro) +- **Licensing:** Only enforce total number of users. (Enterprise) +- **Loki:** do not convert NaN to null. [#45389](https://github.com/grafana/grafana/pull/45389), [@gabor](https://github.com/gabor) +- **Report:** API support for multiple dashboards. (Enterprise) +- **Report:** Support sending embedded image in the report email. (Enterprise) +- **Report:** UI for multiple dashboards. (Enterprise) +- **Reporting:** Remove redundant empty attachment when export to CSV is enabled. (Enterprise) +- **SAML:** Implement Name Templates for assertion_attribute_name option. (Enterprise) +- **SSE/Alerting:** Support prom instant vector responses. [#44865](https://github.com/grafana/grafana/pull/44865), [@kylebrandt](https://github.com/kylebrandt) +- **Tracing:** Add trace to metrics config behind feature toggle. [#46298](https://github.com/grafana/grafana/pull/46298), [@connorlindsey](https://github.com/connorlindsey) + +### Bug fixes + +- **Fix:** Prevent automatic parsing of string data types to numbers. [#46035](https://github.com/grafana/grafana/pull/46035), [@joshhunt](https://github.com/joshhunt) +- **Prometheus:** Fix inconsistent labels in exemplars resulting in marshal json error. [#46135](https://github.com/grafana/grafana/pull/46135), [@hanjm](https://github.com/hanjm) + +### Breaking changes + +In the Loki data source, for consistency and performance reasons, we changed how we represent `NaN` (not a number) values received from Loki. In the past versions, we converted these to `null` in the frontend (for dashboard and explore), and kept as `NaN` in the alerting path. Starting with this version, we will always keep it as `NaN`. This change should be mostly invisible for the users. Issue [#45389](https://github.com/grafana/grafana/issues/45389) + +The dependency to [grafana/aws-sdk](https://github.com/grafana/grafana-aws-sdk-react) is moved from [grafana/ui](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/package.json) to the plugin. This means that any plugin that use SIGV4 auth need to pass a SIGV4 editor component as a prop to the `DataSourceHttpSettings` component. Issue [#43559](https://github.com/grafana/grafana/issues/43559) From 8166d7dc4d70b45ab3a76ba25e1d83a380d5d0f7 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 24 May 2022 15:34:47 +0100 Subject: [PATCH 04/15] Fix escape in Modal/DashboardSettings + add some unit tests (#49500) --- .../src/components/Drawer/Drawer.tsx | 2 + .../src/components/Modal/Modal.test.tsx | 19 +++++++ .../grafana-ui/src/components/Modal/Modal.tsx | 2 +- .../DashboardSettings.test.tsx | 51 +++++++++++++++++++ .../DashboardSettings/DashboardSettings.tsx | 8 ++- 5 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx index 7bf729d0334..55b61dc0cd5 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx @@ -55,6 +55,8 @@ export function Drawer({ const { overlayProps } = useOverlay( { isDismissable: true, + isOpen, + onClose, }, overlayRef ); diff --git a/packages/grafana-ui/src/components/Modal/Modal.test.tsx b/packages/grafana-ui/src/components/Modal/Modal.test.tsx index 08d558d395b..7f4a79d8480 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.test.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { Modal } from './Modal'; @@ -22,4 +23,22 @@ describe('Modal', () => { expect(screen.getByTestId('modal-content')).toBeInTheDocument(); }); + + it('pressing escape calls onDismiss correctly', async () => { + const onDismiss = jest.fn(); + + render( + +
Content
+
+ ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByLabelText('Some Title')).toBeInTheDocument(); + expect(screen.getByTestId('modal-content')).toBeInTheDocument(); + + await userEvent.keyboard('{Escape}'); + + expect(onDismiss).toHaveBeenCalled(); + }); }); diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index 8b2e6adc175..a3f6aeba5df 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -53,7 +53,7 @@ export function Modal(props: PropsWithChildren) { // Handle interacting outside the dialog and pressing // the Escape key to close the modal. const { overlayProps, underlayProps } = useOverlay( - { isKeyboardDismissDisabled: closeOnEscape, isOpen, onClose: onDismiss }, + { isKeyboardDismissDisabled: !closeOnEscape, isOpen, onClose: onDismiss }, ref ); diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx new file mode 100644 index 00000000000..a694be7c3f6 --- /dev/null +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; + +import { locationService, setBackendSrv } from '@grafana/runtime'; +import { configureStore } from 'app/store/configureStore'; + +import { DashboardModel } from '../../state'; + +import { DashboardSettings } from './DashboardSettings'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + locationService: { + partial: jest.fn(), + }, +})); + +setBackendSrv({ + get: jest.fn().mockResolvedValue({}), +} as any); + +describe('DashboardSettings', () => { + it('pressing escape navigates away correctly', async () => { + jest.spyOn(locationService, 'partial'); + const dashboard = new DashboardModel( + { + title: 'Foo', + }, + { + folderId: 1, + } + ); + const store = configureStore(); + render( + + + + + + ); + + expect(screen.getByText('Foo / Settings')).toBeInTheDocument(); + + await userEvent.keyboard('{Escape}'); + + expect(locationService.partial).toHaveBeenCalledWith({ editview: null }); + }); +}); diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index d8494fe6c59..363eae8acc6 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -49,7 +49,13 @@ const MakeEditable = (props: { onMakeEditable: () => any }) => ( export function DashboardSettings({ dashboard, editview }: Props) { const ref = useRef(null); - const { overlayProps } = useOverlay({}, ref); + const { overlayProps } = useOverlay( + { + isOpen: true, + onClose, + }, + ref + ); const { dialogProps } = useDialog( { 'aria-label': 'Dashboard settings', From d373beeb73190241c45ae20fe9d190368f7d8dee Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 24 May 2022 17:43:09 +0300 Subject: [PATCH 05/15] DatePicker: able to set minimum date that can be selected (#49503) --- .../components/DateTimePickers/DatePicker/DatePicker.tsx | 8 +++++++- .../DatePickerWithInput/DatePickerWithInput.tsx | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx index 8e2397df452..e39ce8157ce 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx @@ -15,6 +15,7 @@ export interface DatePickerProps { onClose: () => void; onChange: (value: Date) => void; value?: Date; + minDate?: Date; } /** @public */ @@ -37,7 +38,7 @@ export const DatePicker = memo((props) => { DatePicker.displayName = 'DatePicker'; -const Body = memo(({ value, onChange }) => { +const Body = memo(({ value, minDate, onChange }) => { const styles = useStyles2(getBodyStyles); return ( @@ -45,6 +46,7 @@ const Body = memo(({ value, onChange }) => { className={styles.body} tileClassName={styles.title} value={value || new Date()} + minDate={minDate} nextLabel={} prevLabel={} onChange={(ev: Date | Date[]) => { @@ -68,6 +70,10 @@ export const getStyles = (theme: GrafanaTheme2) => { background-color: ${theme.colors.background.primary}; border: 1px solid ${theme.colors.border.weak}; border-radius: 2px 0 0 2px; + + button:disabled { + color: ${theme.colors.text.disabled}; + } `, }; }; diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx index ac924d7be24..136cfedce82 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx @@ -12,6 +12,7 @@ export const formatDate = (date: Date | string) => dateTime(date).format('L'); /** @public */ export interface DatePickerWithInputProps extends Omit { value?: Date | string; + minDate?: Date; onChange: (value: Date | string) => void; /** Hide the calendar when date is selected */ closeOnSelect?: boolean; @@ -21,6 +22,7 @@ export interface DatePickerWithInputProps extends Omit { onChange(ev); if (closeOnSelect) { From 635fa4ab0f349a9e7a6baa09c69414960c57a44e Mon Sep 17 00:00:00 2001 From: Will Browne Date: Tue, 24 May 2022 17:02:47 +0200 Subject: [PATCH 06/15] Alerting: Validate alert notification UID length (#45546) * validate alert notification UID length * simplify-ish * remove unused field * whoopsie * delete newline * remove check * apply feedback --- pkg/services/alerting/service.go | 13 +++++++++++++ pkg/services/alerting/service_test.go | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pkg/services/alerting/service.go b/pkg/services/alerting/service.go index 34e683561fc..7c30b0a635f 100644 --- a/pkg/services/alerting/service.go +++ b/pkg/services/alerting/service.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type AlertNotificationService struct { @@ -33,6 +34,10 @@ func (s *AlertNotificationService) GetAlertNotifications(ctx context.Context, qu } func (s *AlertNotificationService) CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error { + if util.IsShortUIDTooLong(cmd.Uid) { + return ValidationError{Reason: "Invalid UID: Must be 40 characters or less"} + } + var err error cmd.EncryptedSecureSettings, err = s.EncryptionService.EncryptJsonData(ctx, cmd.SecureSettings, setting.SecretKey) if err != nil { @@ -53,6 +58,10 @@ func (s *AlertNotificationService) CreateAlertNotificationCommand(ctx context.Co } func (s *AlertNotificationService) UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error { + if util.IsShortUIDTooLong(cmd.Uid) { + return ValidationError{Reason: "Invalid UID: Must be 40 characters or less"} + } + var err error cmd.EncryptedSecureSettings, err = s.EncryptionService.EncryptJsonData(ctx, cmd.SecureSettings, setting.SecretKey) if err != nil { @@ -99,6 +108,10 @@ func (s *AlertNotificationService) GetAlertNotificationsWithUid(ctx context.Cont } func (s *AlertNotificationService) UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error { + if util.IsShortUIDTooLong(cmd.Uid) || util.IsShortUIDTooLong(cmd.NewUid) { + return ValidationError{Reason: "Invalid UID: Must be 40 characters or less"} + } + return s.SQLStore.UpdateAlertNotificationWithUid(ctx, cmd) } diff --git a/pkg/services/alerting/service_test.go b/pkg/services/alerting/service_test.go index edd58a5da30..0d8e19245a3 100644 --- a/pkg/services/alerting/service_test.go +++ b/pkg/services/alerting/service_test.go @@ -2,6 +2,7 @@ package alerting import ( "context" + "strings" "testing" "github.com/grafana/grafana/pkg/components/simplejson" @@ -112,6 +113,26 @@ func TestService(t *testing.T) { err = s.DeleteAlertNotification(context.Background(), &delCmd) require.NoError(t, err) }) + + t.Run("create alert notification should reject an invalid command", func(t *testing.T) { + uid := strings.Repeat("A", 41) + + err := s.CreateAlertNotificationCommand(context.Background(), &models.CreateAlertNotificationCommand{Uid: uid}) + require.ErrorIs(t, err, ValidationError{Reason: "Invalid UID: Must be 40 characters or less"}) + }) + + t.Run("update alert notification should reject an invalid command", func(t *testing.T) { + ctx := context.Background() + + uid := strings.Repeat("A", 41) + expectedErr := ValidationError{Reason: "Invalid UID: Must be 40 characters or less"} + + err := s.UpdateAlertNotification(ctx, &models.UpdateAlertNotificationCommand{Uid: uid}) + require.ErrorIs(t, err, expectedErr) + + err = s.UpdateAlertNotificationWithUid(ctx, &models.UpdateAlertNotificationWithUidCommand{NewUid: uid}) + require.ErrorIs(t, err, expectedErr) + }) } func registerTestNotifier(notifierType string) { From 1c0019285f4495a29c17196a887f93089c6b9774 Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Tue, 24 May 2022 17:31:57 +0200 Subject: [PATCH 07/15] Alerting: Improve docs on alert state and health (#49410) --- .../alerting/fundamentals/state-and-health.md | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/docs/sources/alerting/fundamentals/state-and-health.md b/docs/sources/alerting/fundamentals/state-and-health.md index 796f8e9c2b7..a9e4c92f4b8 100644 --- a/docs/sources/alerting/fundamentals/state-and-health.md +++ b/docs/sources/alerting/fundamentals/state-and-health.md @@ -7,24 +7,51 @@ title = "State and health of alerting rules" # State and health of alerting rules -The state and health of alerting rules help you understand several key status indicators about your alerts. There are three key components: alert state, alerting rule state, and alerting rule health. Although related, each component conveys subtly different information. +The state and health of alerting rules help you understand several key status indicators about your alerts. -## Alerting rule state +There are three key components: [alert rule state](#alert-rule-state), [alert instance state](#alert-instance-state), and [alert rule health](#alert-rule-health). Although related, each component conveys subtly different information. -- **Normal**: None of the time series returned by the evaluation engine is in a Pending or Firing state. -- **Pending**: At least one time series returned by the evaluation engine is Pending. -- **Firing**: At least one time series returned by the evaluation engine is Firing. +## Alert rule state -## Alert state +An alert rule can be in either of the following states: -- **Normal**: Condition for the alerting rule is **false** for every time series returned by the evaluation engine. -- **Alerting**: Condition of the alerting rule is **true** for at least one time series returned by the evaluation engine. The duration for which the condition must be true before an alert fires, if set, is met or has exceeded. -- **Pending**: Condition of the alerting rule is **true** for at least one time series returned by the evaluation engine. The duration for which the condition must be true before an alert fires, if set, **has not** been met. -- **NoData**: the alerting rule has not returned a time series, all values for the time series are null, or all values for the time series are zero. -- **Error**: Error when attempting to evaluate an alerting rule. +| State | Description | +| ----------- | ---------------------------------------------------------------------------------------------- | +| **Normal** | None of the time series returned by the evaluation engine is in a `Pending` or `Firing` state. | +| **Pending** | At least one time series returned by the evaluation engine is `Pending`. | +| **Firing** | At least one time series returned by the evaluation engine is `Firing`. | -## Alerting rule health +> **Note:** Alerts will transition first to `pending` and then `firing`, thus it will take at least two evaluation cycles before an alert is fired. -- **Ok**: No error when evaluating an alerting rule. -- **Error**: Error when evaluating an alerting rule. -- **NoData**: The absence of data in at least one time series returned during a rule evaluation. +## Alert instance state + +An alert instance can be in either of the following states: + +| State | Description | +| ------------ | --------------------------------------------------------------------------------------------- | +| **Normal** | The state of an alert that is neither firing nor pending, everything is working correctly. | +| **Pending** | The state of an alert that has been active for less than the configured threshold duration. | +| **Alerting** | The state of an alert that has been active for longer than the configured threshold duration. | +| **NoData** | No data has been received for the configured time window. | +| **Error** | The error that occurred when attempting to evaluate an alerting rule. | + +## Alert rule health + +An alert rule can have one the following health statuses: + +| State | Description | +| ---------- | ---------------------------------------------------------------------------------- | +| **Ok** | No error when evaluating an alerting rule. | +| **Error** | An error occurred when evaluating an alerting rule. | +| **NoData** | The absence of data in at least one time series returned during a rule evaluation. | + +## Special alerts for `NoData` and `Error` + +When evaluation of an alerting rule produces state `NoData` or `Error`, Grafana alerting will generate alert instances that have the following additional labels: + +| Label | Description | +| ------------------ | ---------------------------------------------------------------------- | +| **alertname** | Either `DatasourceNoData` or `DatasourceError` depending on the state. | +| **datasource_uid** | The UID of the data source that caused the state. | + +You can handle these alerts the same way as regular alerts by adding a silence, route to a contact point, and so on. From 4124294011a54fa3bf209dcf2cf8b38a71abf1d4 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Tue, 24 May 2022 17:43:58 +0200 Subject: [PATCH 08/15] Prometheus: Migrate annotation editor to react (#48814) * Modify the annotation support api * Migrate annotation editor component * Update public/app/features/annotations/standardAnnotationSupport.ts Co-authored-by: Ryan McKinley * Move the escape hatches out of the public API * Fix props transforms * Break import cycle Co-authored-by: Ryan McKinley --- .../grafana-data/src/types/annotations.ts | 10 +- .../StandardAnnotationQueryEditor.tsx | 21 ++- .../annotations/standardAnnotationSupport.ts | 28 +++- .../AnnotationsQueryRunner.ts | 2 +- .../LegacyAnnotationQueryRunner.ts | 5 + .../components/LokiQueryEditorSelector.tsx | 10 +- .../components/AnnotationQueryEditor.tsx | 121 ++++++++++++++++++ .../datasource/prometheus/datasource.tsx | 9 ++ .../components/LabelParamEditor.tsx | 5 +- .../PromQueryBuilderOptions.test.tsx | 8 ++ .../components/PromQueryBuilderOptions.tsx | 105 +++++++++------ .../components/PromQueryEditorSelector.tsx | 62 +++++++-- .../shared/QueryEditorModeToggle.tsx | 10 +- 13 files changed, 324 insertions(+), 72 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx diff --git a/packages/grafana-data/src/types/annotations.ts b/packages/grafana-data/src/types/annotations.ts index e701b6b3841..f5c3a72c51b 100644 --- a/packages/grafana-data/src/types/annotations.ts +++ b/packages/grafana-data/src/types/annotations.ts @@ -77,6 +77,12 @@ export interface AnnotationEventFieldMapping { } export type AnnotationEventMappings = Partial>; +type AnnotationQueryEditorProps = QueryEditorProps & { + // Needs to be optional otherwise component not using these cannot be used, even though they are passed on and can be + // just ignored if not used. + annotation?: AnnotationQuery; + onAnnotationChange?: (annotation: AnnotationQuery) => void; +}; /** * Since Grafana 7.2 @@ -86,7 +92,7 @@ export type AnnotationEventMappings = Partial> { /** * This hook lets you manipulate any existing stored values before running them though the processor. - * This is particularly helpful when dealing with migrating old formats. ie query as a string vs object + * This is particularly helpful when dealing with migrating old formats. ie query as a string vs object. */ prepareAnnotation?(json: any): TAnno; @@ -105,5 +111,5 @@ export interface AnnotationSupport>; + QueryEditor?: ComponentType>; } diff --git a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx index 3cf223b504c..3d4b60e2fad 100644 --- a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx +++ b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx @@ -9,7 +9,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { PanelModel } from 'app/features/dashboard/state'; import { executeAnnotationQuery } from '../executeAnnotationQuery'; -import { standardAnnotationSupport } from '../standardAnnotationSupport'; +import { shouldUseLegacyRunner, shouldUseMappingUI, standardAnnotationSupport } from '../standardAnnotationSupport'; import { AnnotationQueryResponse } from '../types'; import { AnnotationFieldMapper } from './AnnotationResultMapper'; @@ -33,7 +33,7 @@ export default class StandardAnnotationQueryEditor extends PureComponent { const { datasource, annotation } = this.props; + if (shouldUseLegacyRunner(datasource)) { + // In the new UI the running of query is done so the data can be mapped. In the legacy annotations this does + // not exist as the annotationQuery already returns annotation events which cannot be mapped. This means that + // right now running a query for data source with legacy runner does not make much sense. + return; + } + const dashboard = getDashboardSrv().getCurrent(); if (!dashboard) { return; @@ -156,11 +163,15 @@ export default class StandardAnnotationQueryEditor extends PureComponent { + this.props.onChange(annotation); + }; + render() { const { datasource, annotation } = this.props; const { response } = this.state; - // Find the annotaiton runner + // Find the annotation runner let QueryEditor = datasource.annotations?.QueryEditor || datasource.components?.QueryEditor; if (!QueryEditor) { return
Annotations are not supported. This datasource needs to export a QueryEditor
; @@ -177,8 +188,10 @@ export default class StandardAnnotationQueryEditor extends PureComponent - {datasource.type !== 'datasource' && ( + {shouldUseMappingUI(datasource) && ( <> {this.renderStatus()} diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts index 079a506a94b..710ded708ed 100644 --- a/public/app/features/annotations/standardAnnotationSupport.ts +++ b/public/app/features/annotations/standardAnnotationSupport.ts @@ -9,6 +9,7 @@ import { AnnotationQuery, AnnotationSupport, DataFrame, + DataSourceApi, Field, FieldType, getFieldDisplayName, @@ -26,6 +27,7 @@ export const standardAnnotationSupport: AnnotationSupport = { return { ...rest, target: { + refId: 'annotation_query', query, }, mappings: {}, @@ -35,14 +37,12 @@ export const standardAnnotationSupport: AnnotationSupport = { }, /** - * Convert the stored JSON model and environment to a standard data source query object. - * This query will be executed in the data source and the results converted into events. - * Returning an undefined result will quietly skip query execution + * Default will just return target from the annotation. */ prepareQuery: (anno: AnnotationQuery) => anno.target, /** - * When the standard frame > event processing is insufficient, this allows explicit control of the mappings + * Provides default processing from dataFrame to annotation events. */ processEvents: (anno: AnnotationQuery, data: DataFrame[]) => { return getAnnotationsFromData(data, anno.mappings); @@ -50,7 +50,7 @@ export const standardAnnotationSupport: AnnotationSupport = { }; /** - * Flatten all panel data into a single frame + * Flatten all frames into a single frame with mergeTransformer. */ export function singleFrameFromPanelData(): OperatorFunction { @@ -226,3 +226,21 @@ export function getAnnotationsFromData( }) ); } + +// These opt outs are here only for quicker and easier migration to react based annotations editors and because +// annotation support API needs some work to support less "standard" editors like prometheus and here it is not +// polluting public API. + +/** + * Opt out of using the default mapping functionality on frontend. + */ +export function shouldUseMappingUI(datasource: DataSourceApi): boolean { + return datasource.type !== 'prometheus'; +} + +/** + * Use legacy runner. Used only as an escape hatch for easier transition to React based annotation editor. + */ +export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean { + return datasource.type === 'prometheus'; +} diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts index 4eadff44e59..05b5bf7a3bc 100644 --- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts +++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts @@ -15,7 +15,7 @@ export class AnnotationsQueryRunner implements AnnotationQueryRunner { return false; } - return !Boolean(datasource.annotationQuery && !datasource.annotations); + return Boolean(!datasource.annotationQuery || datasource.annotations); } run({ annotation, datasource, dashboard, range }: AnnotationQueryRunnerOptions): Observable { diff --git a/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts index 5d94911b9ab..f383897f5d9 100644 --- a/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts +++ b/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts @@ -2,6 +2,7 @@ import { from, Observable, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { AnnotationEvent, DataSourceApi } from '@grafana/data'; +import { shouldUseLegacyRunner } from 'app/features/annotations/standardAnnotationSupport'; import { AnnotationQueryRunner, AnnotationQueryRunnerOptions } from './types'; import { handleAnnotationQueryRunnerError } from './utils'; @@ -12,6 +13,10 @@ export class LegacyAnnotationQueryRunner implements AnnotationQueryRunner { return false; } + if (shouldUseLegacyRunner(datasource)) { + return true; + } + return Boolean(datasource.annotationQuery && !datasource.annotations); } diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index 4f43cf5e0d3..fbb5f1d4f82 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -99,7 +99,15 @@ export const LokiQueryEditorSelector = React.memo((props) > Run query - + diff --git a/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx new file mode 100644 index 00000000000..cd7bfc987d4 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx @@ -0,0 +1,121 @@ +import React from 'react'; + +import { AnnotationQuery } from '@grafana/data'; +import { EditorRow, EditorField, EditorSwitch, Space } from '@grafana/experimental'; +import { Input } from '@grafana/ui'; + +import { PromQueryEditorSelector } from '../querybuilder/components/PromQueryEditorSelector'; +import { QueryEditorMode } from '../querybuilder/shared/types'; +import { PromQuery } from '../types'; + +import { PromQueryEditorProps } from './types'; + +type Props = PromQueryEditorProps & { + annotation?: AnnotationQuery; + onAnnotationChange?: (annotation: AnnotationQuery) => void; +}; + +export function AnnotationQueryEditor(props: Props) { + // This is because of problematic typing. See AnnotationQueryEditorProps in grafana-data/annotations.ts. + const annotation = props.annotation!; + const onAnnotationChange = props.onAnnotationChange!; + return ( + <> + + onAnnotationChange({ + ...annotation, + expr: query.expr, + step: query.interval, + }) + } + uiOptions={{ + modes: { + [QueryEditorMode.Explain]: false, + [QueryEditorMode.Code]: true, + [QueryEditorMode.Builder]: true, + }, + runQueryButton: false, + options: { + exemplars: false, + type: false, + format: false, + minStep: true, + legend: false, + resolution: false, + }, + }} + /> + + + + { + onAnnotationChange({ + ...annotation, + titleFormat: event.currentTarget.value, + }); + }} + /> + + + { + onAnnotationChange({ + ...annotation, + tagKeys: event.currentTarget.value, + }); + }} + /> + + + { + onAnnotationChange({ + ...annotation, + textFormat: event.currentTarget.value, + }); + }} + /> + + + { + onAnnotationChange({ + ...annotation, + useValueForTime: event.currentTarget.value, + }); + }} + /> + + + + ); +} diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index 420d203c817..209112270d2 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -40,6 +40,7 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr import { PromApplication, PromApiFeatures } from 'app/types/unified-alerting-dto'; import { addLabelToQuery } from './add_label_to_query'; +import { AnnotationQueryEditor } from './components/AnnotationQueryEditor'; import PrometheusLanguageProvider from './language_provider'; import { expandRecordingRules } from './language_utils'; import { renderLegendFormat } from './legend'; @@ -119,6 +120,14 @@ export class PrometheusDatasource this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters); this.variables = new PrometheusVariableSupport(this, this.templateSrv, this.timeSrv); this.exemplarsAvailable = true; + + // This needs to be here and cannot be static because of how annotations typing affects casting of data source + // objects to DataSourceApi types. + // We don't use the default processing for prometheus. + // See standardAnnotationSupport.ts/[shouldUseMappingUI|shouldUseLegacyRunner] + this.annotations = { + QueryEditor: AnnotationQueryEditor, + }; } init = async () => { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx index ab95e2798e4..941bb1e11f3 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx @@ -3,7 +3,6 @@ import React, { useState } from 'react'; import { DataSourceApi, SelectableValue, toOption } from '@grafana/data'; import { Select } from '@grafana/ui'; -import { PrometheusDatasource } from '../../datasource'; import { promQueryModeller } from '../PromQueryModeller'; import { getOperationParamId } from '../shared/operationUtils'; import { QueryBuilderLabelFilter, QueryBuilderOperationParamEditorProps } from '../shared/types'; @@ -49,8 +48,8 @@ async function loadGroupByLabels( ): Promise>> { let labels: QueryBuilderLabelFilter[] = query.labels; - // This function is used by both Prometheus and Loki and this the only difference - if (datasource instanceof PrometheusDatasource) { + // This function is used by both Prometheus and Loki and this the only difference. + if (datasource.type === 'prometheus') { labels = [{ label: '__name__', op: '=', value: query.metric }, ...query.labels]; } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx index a4df0c00284..9fc5fe05d38 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx @@ -78,6 +78,14 @@ function setup(queryOverrides: Partial = {}) { }, onRunQuery: jest.fn(), onChange: jest.fn(), + uiOptions: { + exemplars: true, + type: true, + format: true, + minStep: true, + legend: true, + resolution: true, + }, }; const { container } = render(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx index 67ef8b17e7b..17b3b296eea 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx @@ -12,14 +12,24 @@ import { QueryOptionGroup } from '../shared/QueryOptionGroup'; import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor'; +export interface UIOptions { + exemplars: boolean; + type: boolean; + format: boolean; + minStep: boolean; + legend: boolean; + resolution: boolean; +} + export interface Props { query: PromQuery; app?: CoreApp; onChange: (update: PromQuery) => void; onRunQuery: () => void; + uiOptions: UIOptions; } -export const PromQueryBuilderOptions = React.memo(({ query, app, onChange, onRunQuery }) => { +export const PromQueryBuilderOptions = React.memo(({ query, app, onChange, onRunQuery, uiOptions }) => { const onChangeFormat = (value: SelectableValue) => { onChange({ ...query, format: value.value }); onRunQuery(); @@ -50,42 +60,53 @@ export const PromQueryBuilderOptions = React.memo(({ query, app, onChange return ( - - onChange({ ...query, legendFormat })} - onRunQuery={onRunQuery} - /> - - An additional lower limit for the step parameter of the Prometheus query and for the{' '} - $__interval and $__rate_interval variables. - - } - > - + {uiOptions.legend && ( + onChange({ ...query, legendFormat })} + onRunQuery={onRunQuery} /> - - - + + )} + {uiOptions.type && ( + + + + )} + {uiOptions.exemplars && shouldShowExemplarSwitch(query, app) && ( )} - {query.intervalFactor && query.intervalFactor > 1 && ( + {uiOptions.resolution && query.intervalFactor && query.intervalFactor > 1 && (