mirror of https://github.com/rancher/dashboard.git
303 lines
8.1 KiB
TypeScript
303 lines
8.1 KiB
TypeScript
import { md5 } from '@shell/utils/crypto';
|
|
import { randomStr } from '@shell/utils/string';
|
|
import { Notification, StoredNotification } from '@shell/types/notifications';
|
|
import { deriveKey } from '@shell/utils/crypto/encryption';
|
|
import { loadFromString } from '@shell/utils/notifications';
|
|
import { debounce } from 'lodash';
|
|
|
|
/**
|
|
* Key used to store notifications in the browser's local storage
|
|
*/
|
|
const LOCAL_STORAGE_KEY_PREFIX = 'rancher-notifications-';
|
|
|
|
/**
|
|
* Expire notifications in seconds (14 days)
|
|
*/
|
|
const EXPIRY = 14 * 24 * 60 * 60;
|
|
|
|
/**
|
|
* Maximum number of notifications that will be kept
|
|
*/
|
|
const MAX_NOTIFICATIONS = 50;
|
|
|
|
/**
|
|
* Store for the UI Notification Centre
|
|
*/
|
|
interface NotificationsStore {
|
|
localStorageKey: string,
|
|
userId: string;
|
|
notifications: StoredNotification[],
|
|
encryptionKey: CryptoKey | undefined,
|
|
}
|
|
|
|
export const state = function(): NotificationsStore {
|
|
const notifications: StoredNotification[] = [];
|
|
|
|
return {
|
|
localStorageKey: '',
|
|
userId: '',
|
|
encryptionKey: undefined,
|
|
notifications,
|
|
};
|
|
};
|
|
|
|
const debounceSetNotifications = debounce((state: NotificationsStore, notifications: StoredNotification[]) => {
|
|
state.notifications = notifications;
|
|
}, 500);
|
|
|
|
export const getters = {
|
|
all: (state: NotificationsStore) => {
|
|
return state.notifications;
|
|
},
|
|
|
|
item: (state: NotificationsStore) => {
|
|
return (id: string) => {
|
|
return state.notifications.find((i) => i.id === id);
|
|
};
|
|
},
|
|
|
|
// Count of unread notifications
|
|
unreadCount: (state: NotificationsStore) => {
|
|
return state.notifications.filter((n) => !n.read).length;
|
|
},
|
|
|
|
/**
|
|
* Local storage key includes the user key
|
|
*/
|
|
localStorageKey: (state: NotificationsStore) => {
|
|
return state.localStorageKey;
|
|
},
|
|
|
|
userId: (state: NotificationsStore) => {
|
|
return state.userId;
|
|
},
|
|
|
|
encryptionKey: (state: NotificationsStore) => {
|
|
return state.encryptionKey;
|
|
},
|
|
};
|
|
|
|
export const mutations = {
|
|
add(state: NotificationsStore, notification: Notification) {
|
|
if (!notification.id) {
|
|
notification.id = randomStr();
|
|
} else {
|
|
// Check that there is not already a notification with this id
|
|
const index = state.notifications.findIndex((n) => n.id === notification.id);
|
|
|
|
if (index !== -1) {
|
|
console.error(`Can not add a notification with the same id as an existing notification (${ notification.id })`); // eslint-disable-line no-console
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
const stored = {
|
|
...notification,
|
|
read: false,
|
|
created: new Date()
|
|
};
|
|
|
|
// Add to top of list
|
|
state.notifications.unshift(stored);
|
|
|
|
// Check that we have not exceeded the maximum number of notifications
|
|
if (state.notifications.length > MAX_NOTIFICATIONS) {
|
|
state.notifications.pop();
|
|
}
|
|
},
|
|
|
|
markRead(state: NotificationsStore, id: string) {
|
|
const notification = state.notifications.find((n) => n.id === id);
|
|
|
|
if (notification && !notification.read) {
|
|
notification.read = true;
|
|
}
|
|
},
|
|
|
|
markUnread(state: NotificationsStore, id: string) {
|
|
const notification = state.notifications.find((n) => n.id === id);
|
|
|
|
if (notification && notification.read) {
|
|
notification.read = false;
|
|
}
|
|
},
|
|
|
|
markAllRead(state: NotificationsStore) {
|
|
state.notifications.forEach((notification) => {
|
|
if (!notification.read) {
|
|
notification.read = true;
|
|
}
|
|
});
|
|
},
|
|
|
|
update(state: NotificationsStore, notification: Partial<Notification>) {
|
|
if (notification?.id) {
|
|
const index = state.notifications.findIndex((n) => n.id === notification.id);
|
|
|
|
if (index >= 0) {
|
|
state.notifications[index] = {
|
|
...state.notifications[index],
|
|
...notification
|
|
};
|
|
}
|
|
}
|
|
},
|
|
|
|
clearAll(state: NotificationsStore) {
|
|
state.notifications = [];
|
|
},
|
|
|
|
remove(state: NotificationsStore, id: string) {
|
|
state.notifications = state.notifications.filter((n) => n.id !== id);
|
|
},
|
|
|
|
load(state: NotificationsStore, notifications: StoredNotification[]) {
|
|
// On load, check that the data actually is different
|
|
const existingData = JSON.stringify(state.notifications);
|
|
const newData = JSON.stringify(notifications);
|
|
|
|
if (existingData !== newData) {
|
|
debounceSetNotifications(state, notifications);
|
|
}
|
|
},
|
|
|
|
localStorageKey(state: NotificationsStore, userKey: string) {
|
|
state.localStorageKey = `${ LOCAL_STORAGE_KEY_PREFIX }${ userKey }`;
|
|
},
|
|
|
|
userId(state: NotificationsStore, userId: string) {
|
|
state.userId = userId;
|
|
},
|
|
|
|
encryptionKey(state: NotificationsStore, encryptionKey: CryptoKey) {
|
|
state.encryptionKey = encryptionKey;
|
|
},
|
|
};
|
|
|
|
export const actions = {
|
|
add( { commit, dispatch }: any, notification: Notification) {
|
|
commit('add', notification);
|
|
|
|
// Show a growl for the notification if necessary
|
|
dispatch('growl/notification', notification, { root: true });
|
|
},
|
|
|
|
fromGrowl( { commit }: any, notification: Notification) {
|
|
notification.id = randomStr();
|
|
|
|
commit('add', notification);
|
|
|
|
return notification.id;
|
|
},
|
|
|
|
update({ commit }: any, notification: Notification) {
|
|
commit('update', notification);
|
|
},
|
|
|
|
async markRead({ commit, dispatch, getters }: any, id: string) {
|
|
commit('markRead', id);
|
|
|
|
const notification = getters.item(id);
|
|
|
|
if (notification?.preference) {
|
|
await dispatch('prefs/set', notification.preference, { root: true });
|
|
}
|
|
},
|
|
|
|
async markUnread({ commit, dispatch, getters }: any, id: string) {
|
|
commit('markUnread', id);
|
|
|
|
const notification = getters.item(id) as Notification;
|
|
|
|
if (notification?.preference) {
|
|
await dispatch('prefs/set', {
|
|
key: notification.preference.key,
|
|
value: notification.preference.unsetValue || '',
|
|
}, { root: true });
|
|
}
|
|
},
|
|
|
|
async markAllRead({ commit, dispatch, getters }: any) {
|
|
commit('markAllRead');
|
|
|
|
// For all notifications that have a preference, set the preference, since they are now read
|
|
const withPreference = getters.all.filter((n: Notification) => !!n.preference);
|
|
|
|
for (let i = 0; i < withPreference.length; i++) {
|
|
await dispatch('prefs/set', withPreference[i].preference, { root: true });
|
|
}
|
|
},
|
|
|
|
remove({ commit }: any, id: string) {
|
|
commit('remove', id);
|
|
},
|
|
|
|
clearAll({ commit }: any) {
|
|
commit('clearAll');
|
|
},
|
|
|
|
load({ commit }: any, data: StoredNotification[]) {
|
|
commit('load', data);
|
|
},
|
|
|
|
/**
|
|
* Initialize the notifications store and load the notifications from local storage
|
|
*/
|
|
async init({ commit, getters } : any, userData: any) {
|
|
const userKey = userData.id;
|
|
const userId = userData.v3User?.uuid;
|
|
|
|
if (!userKey || !userId) {
|
|
console.error('Unable to initialize notifications - required user info not available'); // eslint-disable-line no-console
|
|
|
|
return;
|
|
}
|
|
|
|
// Notifications are stored under a key for the current user, so set the local storage key based on the user id
|
|
commit('localStorageKey', md5(userKey, 'hex'));
|
|
commit('userId', userId);
|
|
|
|
let notifications: StoredNotification[] = [];
|
|
const localStorageKey = getters['localStorageKey'];
|
|
let encryptionKey;
|
|
|
|
try {
|
|
encryptionKey = await deriveKey(userId);
|
|
} catch (e) {
|
|
console.error('Unable to generate encryption key for notifications', e); // eslint-disable-line no-console
|
|
|
|
return;
|
|
}
|
|
|
|
// Store the encryption key
|
|
commit('encryptionKey', encryptionKey);
|
|
|
|
// Load the notifications from local storage
|
|
try {
|
|
const data = window.localStorage.getItem(localStorageKey) || '{}';
|
|
|
|
notifications = await loadFromString(data, encryptionKey);
|
|
} catch (e) {
|
|
console.error('Unable to read notifications from local storage', e); // eslint-disable-line no-console
|
|
}
|
|
|
|
// Expire old notifications
|
|
const now = new Date();
|
|
|
|
notifications = notifications.filter((n: StoredNotification) => {
|
|
// Try ... catch in case the date parsing fails
|
|
try {
|
|
const created = new Date(n.created);
|
|
const diff = (now.getTime() - created.getTime()) / 1000; // Diff in seconds
|
|
|
|
return diff < EXPIRY;
|
|
} catch (e) {}
|
|
|
|
return true;
|
|
});
|
|
|
|
commit('load', notifications);
|
|
}
|
|
};
|