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