mirror of https://github.com/grafana/grafana
Tempo: Remove duplicated code (#81476)
parent
80d6bf6da0
commit
2fa4ac2a73
@ -1,128 +0,0 @@ |
|||||||
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; |
|
||||||
|
|
||||||
import { AppNotification, AppNotificationSeverity, AppNotificationsState } from './types/appNotifications'; |
|
||||||
|
|
||||||
const MAX_STORED_NOTIFICATIONS = 25; |
|
||||||
export const STORAGE_KEY = 'notifications'; |
|
||||||
export const NEW_NOTIFS_KEY = `${STORAGE_KEY}/lastRead`; |
|
||||||
type StoredNotification = Omit<AppNotification, 'component'>; |
|
||||||
|
|
||||||
export const initialState: AppNotificationsState = { |
|
||||||
byId: deserializeNotifications(), |
|
||||||
lastRead: Number.parseInt(window.localStorage.getItem(NEW_NOTIFS_KEY) ?? `${Date.now()}`, 10), |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Reducer and action to show toast notifications of various types (success, warnings, errors etc). Use to show |
|
||||||
* transient info to user, like errors that cannot be otherwise handled or success after an action. |
|
||||||
* |
|
||||||
* Use factory functions in core/copy/appNotifications to create the payload. |
|
||||||
*/ |
|
||||||
const appNotificationsSlice = createSlice({ |
|
||||||
name: 'appNotifications', |
|
||||||
initialState, |
|
||||||
reducers: { |
|
||||||
notifyApp: (state, { payload: newAlert }: PayloadAction<AppNotification>) => { |
|
||||||
if (Object.values(state.byId).some((alert) => isSimilar(newAlert, alert) && alert.showing)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
state.byId[newAlert.id] = newAlert; |
|
||||||
serializeNotifications(state.byId); |
|
||||||
}, |
|
||||||
hideAppNotification: (state, { payload: alertId }: PayloadAction<string>) => { |
|
||||||
if (!(alertId in state.byId)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
state.byId[alertId].showing = false; |
|
||||||
serializeNotifications(state.byId); |
|
||||||
}, |
|
||||||
clearNotification: (state, { payload: alertId }: PayloadAction<string>) => { |
|
||||||
delete state.byId[alertId]; |
|
||||||
serializeNotifications(state.byId); |
|
||||||
}, |
|
||||||
clearAllNotifications: (state) => { |
|
||||||
state.byId = {}; |
|
||||||
serializeNotifications(state.byId); |
|
||||||
}, |
|
||||||
readAllNotifications: (state, { payload: timestamp }: PayloadAction<number>) => { |
|
||||||
state.lastRead = timestamp; |
|
||||||
}, |
|
||||||
}, |
|
||||||
}); |
|
||||||
|
|
||||||
export const { notifyApp, hideAppNotification, clearNotification, clearAllNotifications, readAllNotifications } = |
|
||||||
appNotificationsSlice.actions; |
|
||||||
|
|
||||||
export const appNotificationsReducer = appNotificationsSlice.reducer; |
|
||||||
|
|
||||||
// Selectors
|
|
||||||
|
|
||||||
export const selectLastReadTimestamp = (state: AppNotificationsState) => state.lastRead; |
|
||||||
export const selectById = (state: AppNotificationsState) => state.byId; |
|
||||||
export const selectAll = createSelector(selectById, (byId) => |
|
||||||
Object.values(byId).sort((a, b) => b.timestamp - a.timestamp) |
|
||||||
); |
|
||||||
export const selectWarningsAndErrors = createSelector(selectAll, (all) => all.filter(isAtLeastWarning)); |
|
||||||
export const selectVisible = createSelector(selectById, (byId) => Object.values(byId).filter((n) => n.showing)); |
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
|
|
||||||
function isSimilar(a: AppNotification, b: AppNotification): boolean { |
|
||||||
return a.icon === b.icon && a.severity === b.severity && a.text === b.text && a.title === b.title; |
|
||||||
} |
|
||||||
|
|
||||||
function isAtLeastWarning(notif: AppNotification) { |
|
||||||
return notif.severity === AppNotificationSeverity.Warning || notif.severity === AppNotificationSeverity.Error; |
|
||||||
} |
|
||||||
|
|
||||||
function isStoredNotification(obj: unknown): obj is StoredNotification { |
|
||||||
return typeof obj === 'object' && obj !== null && 'id' in obj && 'icon' in obj && 'title' in obj && 'text' in obj; |
|
||||||
} |
|
||||||
|
|
||||||
// (De)serialization
|
|
||||||
|
|
||||||
export function deserializeNotifications(): Record<string, StoredNotification> { |
|
||||||
const storedNotifsRaw = window.localStorage.getItem(STORAGE_KEY); |
|
||||||
if (!storedNotifsRaw) { |
|
||||||
return {}; |
|
||||||
} |
|
||||||
|
|
||||||
const parsed = JSON.parse(storedNotifsRaw); |
|
||||||
if (!Object.values(parsed).every((v) => isStoredNotification(v))) { |
|
||||||
return {}; |
|
||||||
} |
|
||||||
|
|
||||||
return parsed; |
|
||||||
} |
|
||||||
|
|
||||||
function serializeNotifications(notifs: Record<string, StoredNotification>) { |
|
||||||
const reducedNotifs = Object.values(notifs) |
|
||||||
.filter(isAtLeastWarning) |
|
||||||
.sort((a, b) => b.timestamp - a.timestamp) |
|
||||||
.slice(0, MAX_STORED_NOTIFICATIONS) |
|
||||||
.reduce<Record<string, StoredNotification>>((prev, cur) => { |
|
||||||
prev[cur.id] = { |
|
||||||
id: cur.id, |
|
||||||
severity: cur.severity, |
|
||||||
icon: cur.icon, |
|
||||||
title: cur.title, |
|
||||||
text: cur.text, |
|
||||||
traceId: cur.traceId, |
|
||||||
timestamp: cur.timestamp, |
|
||||||
// we don't care about still showing toasts after refreshing
|
|
||||||
// https://github.com/grafana/grafana/issues/71932
|
|
||||||
showing: false, |
|
||||||
}; |
|
||||||
|
|
||||||
return prev; |
|
||||||
}, {}); |
|
||||||
|
|
||||||
try { |
|
||||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(reducedNotifs)); |
|
||||||
} catch (err) { |
|
||||||
console.error('Unable to persist notifications to local storage'); |
|
||||||
console.error(err); |
|
||||||
} |
|
||||||
} |
|
@ -1,5 +0,0 @@ |
|||||||
import { appNotificationsReducer as appNotifications } from './appNotification'; |
|
||||||
|
|
||||||
export default { |
|
||||||
appNotifications, |
|
||||||
}; |
|
@ -1,36 +0,0 @@ |
|||||||
export interface AppNotification { |
|
||||||
id: string; |
|
||||||
severity: AppNotificationSeverity; |
|
||||||
icon: string; |
|
||||||
title: string; |
|
||||||
text: string; |
|
||||||
traceId?: string; |
|
||||||
component?: React.ReactElement; |
|
||||||
showing: boolean; |
|
||||||
timestamp: number; |
|
||||||
} |
|
||||||
|
|
||||||
export enum AppNotificationSeverity { |
|
||||||
Success = 'success', |
|
||||||
Warning = 'warning', |
|
||||||
Error = 'error', |
|
||||||
Info = 'info', |
|
||||||
} |
|
||||||
|
|
||||||
export enum AppNotificationTimeout { |
|
||||||
Success = 3000, |
|
||||||
Warning = 5000, |
|
||||||
Error = 7000, |
|
||||||
} |
|
||||||
|
|
||||||
export const timeoutMap = { |
|
||||||
[AppNotificationSeverity.Success]: AppNotificationTimeout.Success, |
|
||||||
[AppNotificationSeverity.Warning]: AppNotificationTimeout.Warning, |
|
||||||
[AppNotificationSeverity.Error]: AppNotificationTimeout.Error, |
|
||||||
[AppNotificationSeverity.Info]: AppNotificationTimeout.Success, |
|
||||||
}; |
|
||||||
|
|
||||||
export interface AppNotificationsState { |
|
||||||
byId: Record<string, AppNotification>; |
|
||||||
lastRead: number; |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
export * from './appNotifications'; |
|
@ -1,46 +0,0 @@ |
|||||||
import { v4 as uuidv4 } from 'uuid'; |
|
||||||
|
|
||||||
import { AppNotification, AppNotificationSeverity } from '../actions/types'; |
|
||||||
|
|
||||||
import { getMessageFromError } from './errors'; |
|
||||||
|
|
||||||
const defaultSuccessNotification = { |
|
||||||
title: '', |
|
||||||
text: '', |
|
||||||
severity: AppNotificationSeverity.Success, |
|
||||||
icon: 'check', |
|
||||||
}; |
|
||||||
|
|
||||||
const defaultErrorNotification = { |
|
||||||
title: '', |
|
||||||
text: '', |
|
||||||
severity: AppNotificationSeverity.Error, |
|
||||||
icon: 'exclamation-triangle', |
|
||||||
}; |
|
||||||
|
|
||||||
export const createSuccessNotification = (title: string, text = '', traceId?: string): AppNotification => ({ |
|
||||||
...defaultSuccessNotification, |
|
||||||
title, |
|
||||||
text, |
|
||||||
id: uuidv4(), |
|
||||||
timestamp: Date.now(), |
|
||||||
showing: true, |
|
||||||
}); |
|
||||||
|
|
||||||
export const createErrorNotification = ( |
|
||||||
title: string, |
|
||||||
text: string | Error = '', |
|
||||||
traceId?: string, |
|
||||||
component?: React.ReactElement |
|
||||||
): AppNotification => { |
|
||||||
return { |
|
||||||
...defaultErrorNotification, |
|
||||||
text: getMessageFromError(text), |
|
||||||
title, |
|
||||||
id: uuidv4(), |
|
||||||
traceId, |
|
||||||
component, |
|
||||||
timestamp: Date.now(), |
|
||||||
showing: true, |
|
||||||
}; |
|
||||||
}; |
|
@ -1,21 +0,0 @@ |
|||||||
import { isFetchError } from '@grafana/runtime'; |
|
||||||
|
|
||||||
export function getMessageFromError(err: unknown): string { |
|
||||||
if (typeof err === 'string') { |
|
||||||
return err; |
|
||||||
} |
|
||||||
|
|
||||||
if (err) { |
|
||||||
if (err instanceof Error) { |
|
||||||
return err.message; |
|
||||||
} else if (isFetchError(err)) { |
|
||||||
if (err.data && err.data.message) { |
|
||||||
return err.data.message; |
|
||||||
} else if (err.statusText) { |
|
||||||
return err.statusText; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return JSON.stringify(err); |
|
||||||
} |
|
@ -1,27 +0,0 @@ |
|||||||
import { Store } from 'redux'; |
|
||||||
|
|
||||||
export let store: Store<StoreState>; |
|
||||||
export const initialKeyedVariablesState: any = { keys: {} }; |
|
||||||
|
|
||||||
type StoreState = ReturnType<ReturnType<any>>; |
|
||||||
|
|
||||||
export function setStore(newStore: Store<StoreState>) { |
|
||||||
store = newStore; |
|
||||||
} |
|
||||||
|
|
||||||
export function getState(): StoreState { |
|
||||||
if (!store || !store.getState) { |
|
||||||
return { templating: { ...initialKeyedVariablesState, lastKey: 'key' } } as StoreState; // used by tests
|
|
||||||
} |
|
||||||
|
|
||||||
return store.getState(); |
|
||||||
} |
|
||||||
|
|
||||||
// This was `any` before
|
|
||||||
export function dispatch(action: any) { |
|
||||||
if (!store || !store.getState) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
return store.dispatch(action); |
|
||||||
} |
|
Loading…
Reference in new issue