UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

179 lines (178 loc) 7.11 kB
/* * Copyright 2025 The Kubernetes Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { createSlice } from '@reduxjs/toolkit'; import _ from 'lodash'; import { getCluster } from '../../../lib/cluster'; /** * The maximum number of notifications to store in localStorage. */ const defaultMaxNotificationsStored = 200; export class Notification { constructor(messageOrOptions, date) { this.cluster = getCluster(); this.date = new Date().getTime(); this.deleted = false; this.message = ''; this.seen = false; if (typeof messageOrOptions === 'string') { console.warn(`Notification constructor with a string arg is deprecated. Please use NotificationOptions as args instead`); if (messageOrOptions) { this.message = this.prepareMessage(messageOrOptions); } if (date) { this.date = date; } } else if (messageOrOptions) { const { message, date, cluster } = messageOrOptions; if (message) { this.message = this.prepareMessage(message); } if (date) { if (date instanceof Date) { this.date = date.getTime(); } else { this.date = date; } } if (cluster) { this.cluster = cluster; } } // generate the id based on the message and the date attached to a notification this.id = btoa(unescape(encodeURIComponent(`${this.date},${this.message},${this.cluster}`))); } prepareMessage(message) { let trimmedMessage = message; if (message && message.length > 250) { // I am not sure if this applies well to all languages, but it should be good enough for now. trimmedMessage = message.slice(0, 249) + '…'; } return trimmedMessage; } static fromJSON(json) { const notification = new Notification({ message: json.message, date: json.date, cluster: json.cluster, }); notification.id = json.id; notification.seen = json.seen; notification.url = json.url; notification.deleted = json.deleted; return notification; } // Avoid marshalling the entire object to JSON, as well as // private properties with the _ prefix. toJSON() { return { id: this.id, seen: this.seen, url: this.url, date: this.date, deleted: this.deleted, cluster: this.cluster, message: this.message, }; } } export const initialState = { notifications: loadNotifications(), }; /** * Store the given notifications to localStorage. * @param notifications - The notifications to store. * @param options - Options for storing notifications. */ function storeNotifications(notifications, options = {}) { const { max = defaultMaxNotificationsStored } = options; const jsonNotifications = notifications .slice(0, max) .map(n => ('toJSON' in n ? n.toJSON() : n)); localStorage.setItem('notifications', JSON.stringify(jsonNotifications)); return jsonNotifications; } /** * @returns An array of NotificationIface objects from localStorage. */ export function loadNotifications() { const localStorageItem = localStorage.getItem('notifications'); const notifications = JSON.parse(localStorageItem || '[]'); // getting an error here .map is not a function here some times, so we return [] to handle this if (!Array.isArray(notifications)) { return []; } return notifications.map((n) => Notification.fromJSON(n).toJSON()); } function mergeNotifications(oldNotifications, newNotifications) { let notifications = _.uniqBy([...newNotifications, ...oldNotifications], 'id'); notifications.sort((n1, n2) => new Date(n2.date).getTime() - new Date(n1.date).getTime()); // We limit the number of notifications here even though we also do it when storing them // so we can check if the notifications are the same and avoid updating them in that case. notifications = notifications.slice(0, defaultMaxNotificationsStored); return notifications; } const notificationsSlice = createSlice({ name: 'notifications', initialState, reducers: { /** * Set notifications. Overwrites current notifications with new notifications. */ setNotifications(state, action) { let notifications = Array.isArray(action.payload) ? action.payload : [action.payload]; if (notifications.length === 0) { const newState = []; storeNotifications(newState); return { notifications: newState, }; } notifications = mergeNotifications(state.notifications, notifications); // Check if the events are the same, if so, don't update the state unless // needed. This saves unnecessary re-renders and may also prevent infinite loops. if (_.isEqual(notifications, state.notifications)) { return state; } return { notifications: storeNotifications(notifications), }; }, /** * Update existing notifications with new notifications data. */ updateNotifications(state, action) { const dispatchedNotifications = Array.isArray(action.payload) ? action.payload : [action.payload]; let updatedState = state.notifications.map(notification => { const updatedNotification = dispatchedNotifications.find(n => n.id === notification.id); return updatedNotification ? Notification.fromJSON({ ...updatedNotification, seen: true }) : notification; }); const newNotifications = dispatchedNotifications.filter(n => !updatedState.some(s => s.id === n.id)); updatedState = mergeNotifications(updatedState, newNotifications); return { notifications: storeNotifications(updatedState), }; }, }, }); export { notificationsSlice, defaultMaxNotificationsStored, storeNotifications }; export const { setNotifications, updateNotifications } = notificationsSlice.actions; export default notificationsSlice.reducer;