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