AppNotifications: Migrate usage of deprecated appEvents.emit method to redux actions (#45607)

This commit is contained in:
kay delaney 2022-02-23 11:31:15 +00:00 committed by GitHub
parent d0d5304662
commit 59317a22e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 163 additions and 119 deletions

View File

@ -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 = {

View File

@ -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', () => {

View File

@ -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,
}),

View File

@ -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') {

View File

@ -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);
});
};

View File

@ -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,
}),

View File

@ -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]
);
}

View File

@ -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);

View File

@ -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
);
}

View File

@ -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',

View File

@ -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)));
}
};
}

View File

@ -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 (

View File

@ -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 (

View File

@ -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"

View File

@ -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 };
};

View File

@ -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 (

View File

@ -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 };
};

View File

@ -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 };
};

View File

@ -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));
};
}

View File

@ -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;
});
};

View File

@ -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)));
}
};
}

View File

@ -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);
}

View File

@ -22,5 +22,5 @@ export enum AppNotificationTimeout {
}
export interface AppNotificationsState {
appNotifications: AppNotification[];
byId: Record<string, AppNotification>;
}

View File

@ -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;

View File

@ -31,5 +31,4 @@ function render(
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
}
export * from '@testing-library/react';
export { render };