mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AppNotifications: Migrate usage of deprecated appEvents.emit method to redux actions (#45607)
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
|
|||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import AppNotificationItem from './AppNotificationItem';
|
import AppNotificationItem from './AppNotificationItem';
|
||||||
import { notifyApp, clearAppNotification } from 'app/core/actions';
|
import { notifyApp, clearAppNotification } from 'app/core/actions';
|
||||||
|
import { selectAll } from 'app/core/reducers/appNotification';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +17,7 @@ import { VerticalGroup } from '@grafana/ui';
|
|||||||
export interface OwnProps {}
|
export interface OwnProps {}
|
||||||
|
|
||||||
const mapStateToProps = (state: StoreState, props: OwnProps) => ({
|
const mapStateToProps = (state: StoreState, props: OwnProps) => ({
|
||||||
appNotifications: state.appNotifications.appNotifications,
|
appNotifications: selectAll(state.appNotifications),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { screen } from '@testing-library/react';
|
||||||
import { render, screen } from 'test/redux-rtl';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { NavModelItem } from '@grafana/data';
|
||||||
|
import { render } from 'test/redux-rtl';
|
||||||
import { NavBarMenu } from './NavBarMenu';
|
import { NavBarMenu } from './NavBarMenu';
|
||||||
|
|
||||||
describe('NavBarMenu', () => {
|
describe('NavBarMenu', () => {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { render } from 'test/redux-rtl';
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
|
|
||||||
import { SignupPage } from './SignupPage';
|
import { SignupPage } from './SignupPage';
|
||||||
|
|
||||||
const postMock = jest.fn();
|
const postMock = jest.fn();
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getBackendSrv: () => ({
|
getBackendSrv: () => ({
|
||||||
post: postMock,
|
post: postMock,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Form, Field, Input, Button, HorizontalGroup, LinkButton, FormAPI } from '@grafana/ui';
|
import { Form, Field, Input, Button, HorizontalGroup, LinkButton, FormAPI } from '@grafana/ui';
|
||||||
import { getConfig } from 'app/core/config';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import appEvents from 'app/core/app_events';
|
import { getConfig } from 'app/core/config';
|
||||||
import { AppEvents } from '@grafana/data';
|
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { InnerBox, LoginLayout } from '../Login/LoginLayout';
|
import { InnerBox, LoginLayout } from '../Login/LoginLayout';
|
||||||
import { PasswordField } from '../PasswordField/PasswordField';
|
import { PasswordField } from '../PasswordField/PasswordField';
|
||||||
|
|
||||||
@@ -26,6 +25,7 @@ interface QueryParams {
|
|||||||
interface Props extends GrafanaRouteComponentProps<{}, QueryParams> {}
|
interface Props extends GrafanaRouteComponentProps<{}, QueryParams> {}
|
||||||
|
|
||||||
export const SignupPage: FC<Props> = (props) => {
|
export const SignupPage: FC<Props> = (props) => {
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
const onSubmit = async (formData: SignupDTO) => {
|
const onSubmit = async (formData: SignupDTO) => {
|
||||||
if (formData.name === '') {
|
if (formData.name === '') {
|
||||||
delete formData.name;
|
delete formData.name;
|
||||||
@@ -43,7 +43,7 @@ export const SignupPage: FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
const msg = err.data?.message || err;
|
const msg = err.data?.message || err;
|
||||||
appEvents.emit(AppEvents.alertWarning, [msg]);
|
notifyApp.warning(msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.code === 'redirect-to-select-org') {
|
if (response.code === 'redirect-to-select-org') {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { Form, Field, Input, Button, Legend, Container, HorizontalGroup, LinkButton } from '@grafana/ui';
|
import { Form, Field, Input, Button, Legend, Container, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||||
import { getConfig } from 'app/core/config';
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import appEvents from 'app/core/app_events';
|
import { getConfig } from 'app/core/config';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
|
|
||||||
interface EmailDTO {
|
interface EmailDTO {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VerifyEmail: FC = () => {
|
export const VerifyEmail: FC = () => {
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
const [emailSent, setEmailSent] = useState(false);
|
const [emailSent, setEmailSent] = useState(false);
|
||||||
|
|
||||||
const onSubmit = (formModel: EmailDTO) => {
|
const onSubmit = (formModel: EmailDTO) => {
|
||||||
@@ -20,7 +20,7 @@ export const VerifyEmail: FC = () => {
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
const msg = err.data?.message || err;
|
const msg = err.data?.message || err;
|
||||||
appEvents.emit(AppEvents.alertWarning, [msg]);
|
notifyApp.warning(msg);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { render } from 'test/redux-rtl';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { VerifyEmailPage } from './VerifyEmailPage';
|
import { VerifyEmailPage } from './VerifyEmailPage';
|
||||||
|
|
||||||
const postMock = jest.fn();
|
const postMock = jest.fn();
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getBackendSrv: () => ({
|
getBackendSrv: () => ({
|
||||||
post: postMock,
|
post: postMock,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
|
import { useMemo } from 'react';
|
||||||
|
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout, useDispatch } from 'app/types';
|
||||||
import { getMessageFromError } from 'app/core/utils/errors';
|
import { getMessageFromError } from 'app/core/utils/errors';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { notifyApp } from '../actions';
|
||||||
|
|
||||||
const defaultSuccessNotification = {
|
const defaultSuccessNotification = {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -53,3 +55,28 @@ export const createWarningNotification = (title: string, text = ''): AppNotifica
|
|||||||
text: text,
|
text: text,
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Hook for showing toast notifications with varying severity (success, warning error).
|
||||||
|
* @example
|
||||||
|
* const notifyApp = useAppNotification();
|
||||||
|
* notifyApp.success('Success!', 'Some additional text');
|
||||||
|
* notifyApp.warning('Warning!');
|
||||||
|
* notifyApp.error('Error!');
|
||||||
|
*/
|
||||||
|
export function useAppNotification() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
success: (title: string, text = '') => {
|
||||||
|
dispatch(notifyApp(createSuccessNotification(title, text)));
|
||||||
|
},
|
||||||
|
warning: (title: string, text = '') => {
|
||||||
|
dispatch(notifyApp(createWarningNotification(title, text)));
|
||||||
|
},
|
||||||
|
error: (title: string, text = '') => {
|
||||||
|
dispatch(notifyApp(createErrorNotification(title, text)));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { appNotificationsReducer, clearAppNotification, notifyApp } from './appNotification';
|
import { appNotificationsReducer, clearAppNotification, notifyApp } from './appNotification';
|
||||||
import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/';
|
import { AppNotificationSeverity, AppNotificationsState, AppNotificationTimeout } from 'app/types/';
|
||||||
|
|
||||||
describe('clear alert', () => {
|
describe('clear alert', () => {
|
||||||
it('should filter alert', () => {
|
it('should filter alert', () => {
|
||||||
const id1 = '1767d3d9-4b99-40eb-ab46-de734a66f21d';
|
const id1 = '1767d3d9-4b99-40eb-ab46-de734a66f21d';
|
||||||
const id2 = '4767b3de-12dd-40e7-b58c-f778bd59d675';
|
const id2 = '4767b3de-12dd-40e7-b58c-f778bd59d675';
|
||||||
|
|
||||||
const initialState = {
|
const initialState: AppNotificationsState = {
|
||||||
appNotifications: [
|
byId: {
|
||||||
{
|
[id1]: {
|
||||||
id: id1,
|
id: id1,
|
||||||
severity: AppNotificationSeverity.Success,
|
severity: AppNotificationSeverity.Success,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
@@ -16,7 +16,7 @@ describe('clear alert', () => {
|
|||||||
text: 'test alert',
|
text: 'test alert',
|
||||||
timeout: AppNotificationTimeout.Success,
|
timeout: AppNotificationTimeout.Success,
|
||||||
},
|
},
|
||||||
{
|
[id2]: {
|
||||||
id: id2,
|
id: id2,
|
||||||
severity: AppNotificationSeverity.Warning,
|
severity: AppNotificationSeverity.Warning,
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
@@ -24,14 +24,14 @@ describe('clear alert', () => {
|
|||||||
text: 'test alert fail 2',
|
text: 'test alert fail 2',
|
||||||
timeout: AppNotificationTimeout.Warning,
|
timeout: AppNotificationTimeout.Warning,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = appNotificationsReducer(initialState, clearAppNotification(id2));
|
const result = appNotificationsReducer(initialState, clearAppNotification(id2));
|
||||||
|
|
||||||
const expectedResult = {
|
const expectedResult: AppNotificationsState = {
|
||||||
appNotifications: [
|
byId: {
|
||||||
{
|
[id1]: {
|
||||||
id: id1,
|
id: id1,
|
||||||
severity: AppNotificationSeverity.Success,
|
severity: AppNotificationSeverity.Success,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
@@ -39,7 +39,7 @@ describe('clear alert', () => {
|
|||||||
text: 'test alert',
|
text: 'test alert',
|
||||||
timeout: AppNotificationTimeout.Success,
|
timeout: AppNotificationTimeout.Success,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
expect(result).toEqual(expectedResult);
|
||||||
@@ -52,9 +52,9 @@ describe('notify', () => {
|
|||||||
const id2 = '4477fcd9-246c-45a5-8818-e22a16683dae';
|
const id2 = '4477fcd9-246c-45a5-8818-e22a16683dae';
|
||||||
const id3 = '55be87a8-bbab-45c7-b481-1f9d46f0d2ee';
|
const id3 = '55be87a8-bbab-45c7-b481-1f9d46f0d2ee';
|
||||||
|
|
||||||
const initialState = {
|
const initialState: AppNotificationsState = {
|
||||||
appNotifications: [
|
byId: {
|
||||||
{
|
[id1]: {
|
||||||
id: id1,
|
id: id1,
|
||||||
severity: AppNotificationSeverity.Success,
|
severity: AppNotificationSeverity.Success,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
@@ -62,7 +62,7 @@ describe('notify', () => {
|
|||||||
text: 'test alert',
|
text: 'test alert',
|
||||||
timeout: AppNotificationTimeout.Success,
|
timeout: AppNotificationTimeout.Success,
|
||||||
},
|
},
|
||||||
{
|
[id2]: {
|
||||||
id: id2,
|
id: id2,
|
||||||
severity: AppNotificationSeverity.Warning,
|
severity: AppNotificationSeverity.Warning,
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
@@ -70,7 +70,7 @@ describe('notify', () => {
|
|||||||
text: 'test alert fail 2',
|
text: 'test alert fail 2',
|
||||||
timeout: AppNotificationTimeout.Warning,
|
timeout: AppNotificationTimeout.Warning,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = appNotificationsReducer(
|
const result = appNotificationsReducer(
|
||||||
@@ -85,9 +85,9 @@ describe('notify', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedResult = {
|
const expectedResult: AppNotificationsState = {
|
||||||
appNotifications: [
|
byId: {
|
||||||
{
|
[id1]: {
|
||||||
id: id1,
|
id: id1,
|
||||||
severity: AppNotificationSeverity.Success,
|
severity: AppNotificationSeverity.Success,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
@@ -95,7 +95,7 @@ describe('notify', () => {
|
|||||||
text: 'test alert',
|
text: 'test alert',
|
||||||
timeout: AppNotificationTimeout.Success,
|
timeout: AppNotificationTimeout.Success,
|
||||||
},
|
},
|
||||||
{
|
[id2]: {
|
||||||
id: id2,
|
id: id2,
|
||||||
severity: AppNotificationSeverity.Warning,
|
severity: AppNotificationSeverity.Warning,
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
@@ -103,7 +103,7 @@ describe('notify', () => {
|
|||||||
text: 'test alert fail 2',
|
text: 'test alert fail 2',
|
||||||
timeout: AppNotificationTimeout.Warning,
|
timeout: AppNotificationTimeout.Warning,
|
||||||
},
|
},
|
||||||
{
|
[id3]: {
|
||||||
id: id3,
|
id: id3,
|
||||||
severity: AppNotificationSeverity.Info,
|
severity: AppNotificationSeverity.Info,
|
||||||
icon: 'info',
|
icon: 'info',
|
||||||
@@ -111,16 +111,16 @@ describe('notify', () => {
|
|||||||
text: 'test alert info 3',
|
text: 'test alert info 3',
|
||||||
timeout: AppNotificationTimeout.Success,
|
timeout: AppNotificationTimeout.Success,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
expect(result).toEqual(expectedResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Dedupe identical alerts', () => {
|
it('Dedupe identical alerts', () => {
|
||||||
const initialState = {
|
const initialState: AppNotificationsState = {
|
||||||
appNotifications: [
|
byId: {
|
||||||
{
|
id1: {
|
||||||
id: 'id1',
|
id: 'id1',
|
||||||
severity: AppNotificationSeverity.Success,
|
severity: AppNotificationSeverity.Success,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
@@ -128,7 +128,7 @@ describe('notify', () => {
|
|||||||
text: 'test alert',
|
text: 'test alert',
|
||||||
timeout: AppNotificationTimeout.Success,
|
timeout: AppNotificationTimeout.Success,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = appNotificationsReducer(
|
const result = appNotificationsReducer(
|
||||||
@@ -143,9 +143,9 @@ describe('notify', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedResult = {
|
const expectedResult: AppNotificationsState = {
|
||||||
appNotifications: [
|
byId: {
|
||||||
{
|
id1: {
|
||||||
id: 'id1',
|
id: 'id1',
|
||||||
severity: AppNotificationSeverity.Success,
|
severity: AppNotificationSeverity.Success,
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
@@ -153,7 +153,7 @@ describe('notify', () => {
|
|||||||
text: 'test alert',
|
text: 'test alert',
|
||||||
timeout: AppNotificationTimeout.Success,
|
timeout: AppNotificationTimeout.Success,
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(result).toEqual(expectedResult);
|
expect(result).toEqual(expectedResult);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import { AppNotification, AppNotificationsState } from 'app/types/';
|
import { AppNotification, AppNotificationsState } from 'app/types/';
|
||||||
|
|
||||||
export const initialState: AppNotificationsState = {
|
export const initialState: AppNotificationsState = {
|
||||||
appNotifications: [] as AppNotification[],
|
byId: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,30 +15,31 @@ const appNotificationsSlice = createSlice({
|
|||||||
name: 'appNotifications',
|
name: 'appNotifications',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
notifyApp: (state, action: PayloadAction<AppNotification>) => {
|
notifyApp: (state, { payload: newAlert }: PayloadAction<AppNotification>) => {
|
||||||
const newAlert = action.payload;
|
if (Object.values(state.byId).some((alert) => isSimilar(newAlert, alert))) {
|
||||||
|
return;
|
||||||
for (const existingAlert of state.appNotifications) {
|
|
||||||
if (
|
|
||||||
newAlert.icon === existingAlert.icon &&
|
|
||||||
newAlert.severity === existingAlert.severity &&
|
|
||||||
newAlert.text === existingAlert.text &&
|
|
||||||
newAlert.title === existingAlert.title &&
|
|
||||||
newAlert.component === existingAlert.component
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.appNotifications.push(newAlert);
|
state.byId[newAlert.id] = newAlert;
|
||||||
|
},
|
||||||
|
clearAppNotification: (state, { payload: alertId }: PayloadAction<string>) => {
|
||||||
|
delete state.byId[alertId];
|
||||||
},
|
},
|
||||||
clearAppNotification: (state, action: PayloadAction<string>): AppNotificationsState => ({
|
|
||||||
...state,
|
|
||||||
appNotifications: state.appNotifications.filter((appNotification) => appNotification.id !== action.payload),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { notifyApp, clearAppNotification } = appNotificationsSlice.actions;
|
export const { notifyApp, clearAppNotification } = appNotificationsSlice.actions;
|
||||||
|
|
||||||
export const appNotificationsReducer = appNotificationsSlice.reducer;
|
export const appNotificationsReducer = appNotificationsSlice.reducer;
|
||||||
|
|
||||||
|
export const selectAll = (state: AppNotificationsState) => Object.values(state.byId);
|
||||||
|
|
||||||
|
function isSimilar(a: AppNotification, b: AppNotification): boolean {
|
||||||
|
return (
|
||||||
|
a.icon === b.icon &&
|
||||||
|
a.severity === b.severity &&
|
||||||
|
a.text === b.text &&
|
||||||
|
a.title === b.title &&
|
||||||
|
a.component === b.component
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import config from 'app/core/config';
|
|||||||
const defaultPins = ['home', 'dashboards', 'explore', 'alerting'].join(',');
|
const defaultPins = ['home', 'dashboards', 'explore', 'alerting'].join(',');
|
||||||
const storedPins = (window.localStorage.getItem('pinnedNavItems') ?? defaultPins).split(',');
|
const storedPins = (window.localStorage.getItem('pinnedNavItems') ?? defaultPins).split(',');
|
||||||
|
|
||||||
export const initialState: NavModelItem[] = (config.bootData.navTree as NavModelItem[]).map((n: NavModelItem) => ({
|
export const initialState: NavModelItem[] = ((config.bootData?.navTree ?? []) as NavModelItem[]).map(
|
||||||
...n,
|
(n: NavModelItem) => ({
|
||||||
hideFromNavbar: n.id === undefined || !storedPins.includes(n.id),
|
...n,
|
||||||
}));
|
hideFromNavbar: n.id === undefined || !storedPins.includes(n.id),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const navTreeSlice = createSlice({
|
const navTreeSlice = createSlice({
|
||||||
name: 'navBarTree',
|
name: 'navBarTree',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AppEvents } from '@grafana/data';
|
|
||||||
import { getBackendSrv, locationService } from '@grafana/runtime';
|
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||||
import { appEvents } from 'app/core/core';
|
|
||||||
import { loadAlertRules, loadedAlertRules, notificationChannelLoaded, setNotificationChannels } from './reducers';
|
import { loadAlertRules, loadedAlertRules, notificationChannelLoaded, setNotificationChannels } from './reducers';
|
||||||
import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
|
import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
|
||||||
|
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
|
||||||
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
|
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
@@ -20,14 +20,14 @@ export function togglePauseAlertRule(id: number, options: { paused: boolean }):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNotificationChannel(data: any): ThunkResult<void> {
|
export function createNotificationChannel(data: any): ThunkResult<Promise<void>> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
await getBackendSrv().post(`/api/alert-notifications`, data);
|
await getBackendSrv().post(`/api/alert-notifications`, data);
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Notification created']);
|
dispatch(notifyApp(createSuccessNotification('Notification created')));
|
||||||
locationService.push('/alerting/notifications');
|
locationService.push('/alerting/notifications');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
appEvents.emit(AppEvents.alertError, [error.data.error]);
|
dispatch(notifyApp(createErrorNotification(error.data.error)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -36,9 +36,9 @@ export function updateNotificationChannel(data: any): ThunkResult<void> {
|
|||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
await getBackendSrv().put(`/api/alert-notifications/${data.id}`, data);
|
await getBackendSrv().put(`/api/alert-notifications/${data.id}`, data);
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Notification updated']);
|
dispatch(notifyApp(createSuccessNotification('Notification updated')));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
appEvents.emit(AppEvents.alertError, [error.data.error]);
|
dispatch(notifyApp(createErrorNotification(error.data.error)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2, AppEvents } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Alert, Button, Field, Input, LinkButton, useStyles2 } from '@grafana/ui';
|
import { Alert, Button, Field, Input, LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
@@ -12,8 +12,8 @@ import { ChannelValues, CommonSettingsComponentType, ReceiverFormValues } from '
|
|||||||
import { makeAMLink } from '../../../utils/misc';
|
import { makeAMLink } from '../../../utils/misc';
|
||||||
import { ChannelSubForm } from './ChannelSubForm';
|
import { ChannelSubForm } from './ChannelSubForm';
|
||||||
import { DeletedSubForm } from './fields/DeletedSubform';
|
import { DeletedSubForm } from './fields/DeletedSubform';
|
||||||
import { appEvents } from 'app/core/core';
|
|
||||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
||||||
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
|
|
||||||
interface Props<R extends ChannelValues> {
|
interface Props<R extends ChannelValues> {
|
||||||
config: AlertManagerCortexConfig;
|
config: AlertManagerCortexConfig;
|
||||||
@@ -38,6 +38,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
|||||||
takenReceiverNames,
|
takenReceiverNames,
|
||||||
commonSettingsComponent,
|
commonSettingsComponent,
|
||||||
}: Props<R>): JSX.Element {
|
}: Props<R>): JSX.Element {
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const readOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
const readOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||||
const defaultValues = initialValues || {
|
const defaultValues = initialValues || {
|
||||||
@@ -84,7 +85,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onInvalid = () => {
|
const onInvalid = () => {
|
||||||
appEvents.emit(AppEvents.alertError, ['There are errors in the form. Please correct them and try again!']);
|
notifyApp.error('There are errors in the form. Please correct them and try again!');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { FC, useMemo, useState } from 'react';
|
import React, { FC, useMemo, useState } from 'react';
|
||||||
import { GrafanaTheme2, AppEvents } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { PageToolbar, Button, useStyles2, CustomScrollbar, Spinner, ConfirmModal } from '@grafana/ui';
|
import { PageToolbar, Button, useStyles2, CustomScrollbar, Spinner, ConfirmModal } from '@grafana/ui';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ import { useCleanup } from 'app/core/hooks/useCleanup';
|
|||||||
import { rulerRuleToFormValues, getDefaultFormValues, getDefaultQueries } from '../../utils/rule-form';
|
import { rulerRuleToFormValues, getDefaultFormValues, getDefaultQueries } from '../../utils/rule-form';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
|
|
||||||
import { appEvents } from 'app/core/core';
|
|
||||||
import { CloudConditionsStep } from './CloudConditionsStep';
|
import { CloudConditionsStep } from './CloudConditionsStep';
|
||||||
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
|
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
|
||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
@@ -31,6 +31,7 @@ type Props = {
|
|||||||
export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
|
|
||||||
const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list';
|
const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list';
|
||||||
@@ -98,7 +99,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onInvalid = () => {
|
const onInvalid = () => {
|
||||||
appEvents.emit(AppEvents.alertError, ['There are errors in the form. Please correct them and try again!']);
|
notifyApp.error('There are errors in the form. Please correct them and try again!');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import React, { FC, Fragment, useState } from 'react';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { AppEvents, GrafanaTheme2, urlUtil } from '@grafana/data';
|
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { Button, ConfirmModal, ClipboardButton, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
import { Button, ConfirmModal, ClipboardButton, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { appEvents } from 'app/core/core';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||||
import { Annotation } from '../../utils/constants';
|
import { Annotation } from '../../utils/constants';
|
||||||
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
@@ -26,6 +26,7 @@ interface Props {
|
|||||||
export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
const style = useStyles2(getStyles);
|
const style = useStyles2(getStyles);
|
||||||
const { namespace, group, rulerRule } = rule;
|
const { namespace, group, rulerRule } = rule;
|
||||||
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
||||||
@@ -189,10 +190,10 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
<ClipboardButton
|
<ClipboardButton
|
||||||
key="copy"
|
key="copy"
|
||||||
onClipboardCopy={() => {
|
onClipboardCopy={() => {
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['URL copied!']);
|
notifyApp.success('URL copied!');
|
||||||
}}
|
}}
|
||||||
onClipboardError={(e) => {
|
onClipboardError={(e) => {
|
||||||
appEvents.emit(AppEvents.alertError, ['Error while copying URL', e.text]);
|
notifyApp.error('Error while copying URL', e.text);
|
||||||
}}
|
}}
|
||||||
className={style.button}
|
className={style.button}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAsyncFn } from 'react-use';
|
import { useAsyncFn } from 'react-use';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
|
import { deleteDashboard } from 'app/features/manage-dashboards/state/actions';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
|
|
||||||
export const useDashboardDelete = (uid: string) => {
|
export const useDashboardDelete = (uid: string) => {
|
||||||
const [state, onDeleteDashboard] = useAsyncFn(() => deleteDashboard(uid, false), []);
|
const [state, onDeleteDashboard] = useAsyncFn(() => deleteDashboard(uid, false), []);
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.value) {
|
if (state.value) {
|
||||||
locationService.replace('/');
|
locationService.replace('/');
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Dashboard Deleted', state.value.title + ' has been deleted']);
|
notifyApp.success('Dashboard Deleted', `${state.value.title} has been deleted`);
|
||||||
}
|
}
|
||||||
}, [state]);
|
}, [state, notifyApp]);
|
||||||
|
|
||||||
return { state, onDeleteDashboard };
|
return { state, onDeleteDashboard };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { css } from '@emotion/css';
|
|||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import { Button, ClipboardButton, Modal, stylesFactory, TextArea, useTheme } from '@grafana/ui';
|
import { Button, ClipboardButton, Modal, stylesFactory, TextArea, useTheme } from '@grafana/ui';
|
||||||
import { SaveDashboardFormProps } from '../types';
|
import { SaveDashboardFormProps } from '../types';
|
||||||
import { AppEvents, GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import appEvents from '../../../../../core/app_events';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
|
|
||||||
export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard, onCancel }) => {
|
export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard, onCancel }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
const [dashboardJSON, setDashboardJson] = useState(() => {
|
const [dashboardJSON, setDashboardJson] = useState(() => {
|
||||||
const clone = dashboard.getSaveModelClone();
|
const clone = dashboard.getSaveModelClone();
|
||||||
delete clone.id;
|
delete clone.id;
|
||||||
@@ -22,8 +23,8 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
|
|||||||
}, [dashboard.title, dashboardJSON]);
|
}, [dashboard.title, dashboardJSON]);
|
||||||
|
|
||||||
const onCopyToClipboardSuccess = useCallback(() => {
|
const onCopyToClipboardSuccess = useCallback(() => {
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Dashboard JSON copied to clipboard']);
|
notifyApp.success('Dashboard JSON copied to clipboard');
|
||||||
}, []);
|
}, [notifyApp]);
|
||||||
|
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||||
import { AppEvents, locationUtil } from '@grafana/data';
|
import { locationUtil } from '@grafana/data';
|
||||||
import { SaveDashboardOptions } from './types';
|
import { SaveDashboardOptions } from './types';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { saveDashboard as saveDashboardApiCall } from 'app/features/manage-dashboards/state/actions';
|
import { saveDashboard as saveDashboardApiCall } from 'app/features/manage-dashboards/state/actions';
|
||||||
import { locationService, reportInteraction } from '@grafana/runtime';
|
import { locationService, reportInteraction } from '@grafana/runtime';
|
||||||
@@ -24,6 +25,7 @@ export const useDashboardSave = (dashboard: DashboardModel) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.value) {
|
if (state.value) {
|
||||||
dashboard.version = state.value.version;
|
dashboard.version = state.value.version;
|
||||||
@@ -31,7 +33,7 @@ export const useDashboardSave = (dashboard: DashboardModel) => {
|
|||||||
|
|
||||||
// important that these happen before location redirect below
|
// important that these happen before location redirect below
|
||||||
appEvents.publish(new DashboardSavedEvent());
|
appEvents.publish(new DashboardSavedEvent());
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Dashboard saved']);
|
notifyApp.success('Dashboard saved');
|
||||||
reportInteraction(`Dashboard ${dashboard.id ? 'saved' : 'created'}`, {
|
reportInteraction(`Dashboard ${dashboard.id ? 'saved' : 'created'}`, {
|
||||||
name: dashboard.title,
|
name: dashboard.title,
|
||||||
url: state.value.url,
|
url: state.value.url,
|
||||||
@@ -44,7 +46,7 @@ export const useDashboardSave = (dashboard: DashboardModel) => {
|
|||||||
setTimeout(() => locationService.replace(newUrl));
|
setTimeout(() => locationService.replace(newUrl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [dashboard, state]);
|
}, [dashboard, state, notifyApp]);
|
||||||
|
|
||||||
return { state, onDashboardSave };
|
return { state, onDashboardSave };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useAsyncFn } from 'react-use';
|
import { useAsyncFn } from 'react-use';
|
||||||
import { AppEvents, locationUtil } from '@grafana/data';
|
import { locationUtil } from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { historySrv } from './HistorySrv';
|
import { historySrv } from './HistorySrv';
|
||||||
import { DashboardModel } from '../../state';
|
import { DashboardModel } from '../../state';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
@@ -15,6 +15,7 @@ const restoreDashboard = async (version: number, dashboard: DashboardModel) => {
|
|||||||
export const useDashboardRestore = (version: number) => {
|
export const useDashboardRestore = (version: number) => {
|
||||||
const dashboard = useSelector((state: StoreState) => state.dashboard.getModel());
|
const dashboard = useSelector((state: StoreState) => state.dashboard.getModel());
|
||||||
const [state, onRestoreDashboard] = useAsyncFn(async () => await restoreDashboard(version, dashboard!), []);
|
const [state, onRestoreDashboard] = useAsyncFn(async () => await restoreDashboard(version, dashboard!), []);
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.value) {
|
if (state.value) {
|
||||||
@@ -26,8 +27,8 @@ export const useDashboardRestore = (version: number) => {
|
|||||||
pathname: newUrl,
|
pathname: newUrl,
|
||||||
state: { routeReloadCounter: prevState ? prevState + 1 : 1 },
|
state: { routeReloadCounter: prevState ? prevState + 1 : 1 },
|
||||||
});
|
});
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Dashboard restored', 'Restored from version ' + version]);
|
notifyApp.success('Dashboard restored', `Restored from version ${version}`);
|
||||||
}
|
}
|
||||||
}, [state, version]);
|
}, [state, version, notifyApp]);
|
||||||
return { state, onRestoreDashboard };
|
return { state, onRestoreDashboard };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { AppEvents, locationUtil } from '@grafana/data';
|
import { locationUtil } from '@grafana/data';
|
||||||
import { getBackendSrv, locationService } from '@grafana/runtime';
|
import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
import { FolderState, ThunkResult } from 'app/types';
|
import { FolderState, ThunkResult } from 'app/types';
|
||||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
|
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
|
||||||
import { notifyApp, updateNavIndex } from 'app/core/actions';
|
import { notifyApp, updateNavIndex } from 'app/core/actions';
|
||||||
|
import { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification';
|
||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import appEvents from 'app/core/app_events';
|
|
||||||
import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers';
|
import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { createWarningNotification } from 'app/core/copy/appNotification';
|
|
||||||
|
|
||||||
export function getFolderByUid(uid: string): ThunkResult<void> {
|
export function getFolderByUid(uid: string): ThunkResult<void> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
@@ -25,8 +24,7 @@ export function saveFolder(folder: FolderState): ThunkResult<void> {
|
|||||||
version: folder.version,
|
version: folder.version,
|
||||||
});
|
});
|
||||||
|
|
||||||
// this should be redux action at some point
|
dispatch(notifyApp(createSuccessNotification('Folder saved')));
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Folder saved']);
|
|
||||||
locationService.push(`${res.url}/settings`);
|
locationService.push(`${res.url}/settings`);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -143,9 +141,9 @@ export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult<v
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createNewFolder(folderName: string): ThunkResult<void> {
|
export function createNewFolder(folderName: string): ThunkResult<void> {
|
||||||
return async () => {
|
return async (dispatch) => {
|
||||||
const newFolder = await getBackendSrv().post('/api/folders', { title: folderName });
|
const newFolder = await getBackendSrv().post('/api/folders', { title: folderName });
|
||||||
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
|
dispatch(notifyApp(createSuccessNotification('Folder Created', 'OK')));
|
||||||
locationService.push(locationUtil.stripBaseFromUrl(newFolder.url));
|
locationService.push(locationUtil.stripBaseFromUrl(newFolder.url));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui';
|
import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui';
|
||||||
import { DataSourcePicker, getBackendSrv } from '@grafana/runtime';
|
import { DataSourcePicker, getBackendSrv } from '@grafana/runtime';
|
||||||
import { AppEvents, DataSourceRef, LiveChannelScope, SelectableValue } from '@grafana/data';
|
import { DataSourceRef, LiveChannelScope, SelectableValue } from '@grafana/data';
|
||||||
import appEvents from 'app/core/app_events';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { Rule } from './types';
|
import { Rule } from './types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -29,14 +29,15 @@ export function AddNewRule({ onRuleAdded }: Props) {
|
|||||||
const [pattern, setPattern] = useState<string>();
|
const [pattern, setPattern] = useState<string>();
|
||||||
const [patternPrefix, setPatternPrefix] = useState<string>('');
|
const [patternPrefix, setPatternPrefix] = useState<string>('');
|
||||||
const [datasource, setDatasource] = useState<DataSourceRef>();
|
const [datasource, setDatasource] = useState<DataSourceRef>();
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
if (!pattern) {
|
if (!pattern) {
|
||||||
appEvents.emit(AppEvents.alertError, ['Enter path']);
|
notifyApp.error('Enter path');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (patternType === 'ds' && !patternPrefix.length) {
|
if (patternType === 'ds' && !patternPrefix.length) {
|
||||||
appEvents.emit(AppEvents.alertError, ['Select datasource']);
|
notifyApp.error('Select datasource');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ export function AddNewRule({ onRuleAdded }: Props) {
|
|||||||
onRuleAdded(v.rule);
|
onRuleAdded(v.rule);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
appEvents.emit(AppEvents.alertError, ['Error adding rule', e]);
|
notifyApp.error('Error adding rule', e);
|
||||||
e.isHandled = true;
|
e.isHandled = true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppEvents, DataSourceInstanceSettings, locationUtil } from '@grafana/data';
|
import { DataSourceInstanceSettings, locationUtil } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
clearDashboard,
|
clearDashboard,
|
||||||
fetchDashboard,
|
fetchDashboard,
|
||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
setLibraryPanelInputs,
|
setLibraryPanelInputs,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { DashboardDataDTO, DashboardDTO, FolderInfo, PermissionLevelString, ThunkResult } from 'app/types';
|
import { DashboardDataDTO, DashboardDTO, FolderInfo, PermissionLevelString, ThunkResult } from 'app/types';
|
||||||
import { appEvents } from '../../../core/core';
|
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||||
import { getDataSourceSrv, locationService, getBackendSrv } from '@grafana/runtime';
|
import { getDataSourceSrv, locationService, getBackendSrv } from '@grafana/runtime';
|
||||||
import { DashboardSearchHit } from '../../search/types';
|
import { DashboardSearchHit } from '../../search/types';
|
||||||
@@ -31,7 +32,7 @@ export function fetchGcomDashboard(id: string): ThunkResult<void> {
|
|||||||
dispatch(processElements(dashboard.json));
|
dispatch(processElements(dashboard.json));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(fetchFailed());
|
dispatch(fetchFailed());
|
||||||
appEvents.emit(AppEvents.alertError, [error.data.message || error]);
|
dispatch(notifyApp(createErrorNotification(error.data.message || error)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Button, HorizontalGroup, Modal, stylesFactory, useTheme } from '@grafana/ui';
|
import { Button, HorizontalGroup, Modal, stylesFactory, useTheme } from '@grafana/ui';
|
||||||
import { AppEvents, GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { FolderInfo } from 'app/types';
|
import { FolderInfo } from 'app/types';
|
||||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
import appEvents from 'app/core/app_events';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { DashboardSection, OnMoveItems } from '../types';
|
import { DashboardSection, OnMoveItems } from '../types';
|
||||||
import { getCheckedDashboards } from '../utils';
|
import { getCheckedDashboards } from '../utils';
|
||||||
import { moveDashboards } from 'app/features/manage-dashboards/state/actions';
|
import { moveDashboards } from 'app/features/manage-dashboards/state/actions';
|
||||||
@@ -21,6 +21,7 @@ export const MoveToFolderModal: FC<Props> = ({ results, onMoveItems, isOpen, onD
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const selectedDashboards = getCheckedDashboards(results);
|
const selectedDashboards = getCheckedDashboards(results);
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
|
|
||||||
const moveTo = () => {
|
const moveTo = () => {
|
||||||
if (folder && selectedDashboards.length) {
|
if (folder && selectedDashboards.length) {
|
||||||
@@ -31,11 +32,11 @@ export const MoveToFolderModal: FC<Props> = ({ results, onMoveItems, isOpen, onD
|
|||||||
const ending = result.successCount === 1 ? '' : 's';
|
const ending = result.successCount === 1 ? '' : 's';
|
||||||
const header = `Dashboard${ending} Moved`;
|
const header = `Dashboard${ending} Moved`;
|
||||||
const msg = `${result.successCount} dashboard${ending} moved to ${folderTitle}`;
|
const msg = `${result.successCount} dashboard${ending} moved to ${folderTitle}`;
|
||||||
appEvents.emit(AppEvents.alertSuccess, [header, msg]);
|
notifyApp.success(header, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.totalCount === result.alreadyInFolderCount) {
|
if (result.totalCount === result.alreadyInFolderCount) {
|
||||||
appEvents.emit(AppEvents.alertError, ['Error', `Dashboard already belongs to folder ${folderTitle}`]);
|
notifyApp.error('Error', `Dashboard already belongs to folder ${folderTitle}`);
|
||||||
} else {
|
} else {
|
||||||
onMoveItems(selectedDashboards, folder);
|
onMoveItems(selectedDashboards, folder);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ export enum AppNotificationTimeout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppNotificationsState {
|
export interface AppNotificationsState {
|
||||||
appNotifications: AppNotification[];
|
byId: Record<string, AppNotification>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type ThunkResult<R> = ThunkAction<R, StoreState, undefined, PayloadAction
|
|||||||
|
|
||||||
export type ThunkDispatch = GenericThunkDispatch<StoreState, undefined, Action>;
|
export type ThunkDispatch = GenericThunkDispatch<StoreState, undefined, Action>;
|
||||||
|
|
||||||
|
// Typed useDispatch & useSelector hooks
|
||||||
export type AppDispatch = ReturnType<typeof configureStore>['dispatch'];
|
export type AppDispatch = ReturnType<typeof configureStore>['dispatch'];
|
||||||
export const useDispatch = () => useDispatchUntyped<AppDispatch>();
|
export const useDispatch = () => useDispatchUntyped<AppDispatch>();
|
||||||
export const useSelector: TypedUseSelectorHook<StoreState> = useSelectorUntyped;
|
export const useSelector: TypedUseSelectorHook<StoreState> = useSelectorUntyped;
|
||||||
|
|||||||
@@ -31,5 +31,4 @@ function render(
|
|||||||
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
|
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from '@testing-library/react';
|
|
||||||
export { render };
|
export { render };
|
||||||
|
|||||||
Reference in New Issue
Block a user