From 0916da0e1c51dd61a3887e365230a654c68fadcb Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Tue, 5 Sep 2023 00:16:04 +0530 Subject: [PATCH 01/44] Created Filtering Signed-off-by: aabidsofi19 Add NotificationCenter Header Signed-off-by: aabidsofi19 disbabel invalid values in filters Signed-off-by: aabidsofi19 --- ui/.eslintrc.js | 113 ++--- ui/assets/icons/AlertIcon.js | 18 + ui/assets/icons/ArchiveIcon.js | 24 + ui/assets/icons/ClearIcon.js | 2 +- ui/assets/icons/ContentFilterIcon.js | 26 ++ ui/assets/icons/CrossCircleIcon.js | 31 ++ ui/components/Header.js | 4 +- ui/components/MesheryNotification.js | 494 --------------------- ui/components/NotificationCenter/filter.js | 459 +++++++++++++++++++ ui/components/NotificationCenter/index.js | 259 +++++++++++ ui/themes/app.js | 6 +- 11 files changed, 888 insertions(+), 548 deletions(-) create mode 100644 ui/assets/icons/AlertIcon.js create mode 100644 ui/assets/icons/ArchiveIcon.js create mode 100644 ui/assets/icons/ContentFilterIcon.js create mode 100644 ui/assets/icons/CrossCircleIcon.js delete mode 100644 ui/components/MesheryNotification.js create mode 100644 ui/components/NotificationCenter/filter.js create mode 100644 ui/components/NotificationCenter/index.js diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 551450c2d31..5300a316331 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,31 +1,31 @@ module.exports = { - "env" : { - "browser" : true, - "es6" : true, - "node" : true // tells the parser that we are using nodejs + env : { + browser : true, + es6 : true, + node : true, // tells the parser that we are using nodejs }, - 'settings' : { - 'react' : { - 'version' : require('./package.json').dependencies.react - } + settings : { + react : { + version : require("./package.json").dependencies.react, + }, }, - "extends" : ["eslint:recommended", "plugin:react/recommended", "plugin:cypress/recommended", "next"], - "globals" : { - "Atomics" : "readonly", - "SharedArrayBuffer" : "readonly", - "globalThis" : "readonly" + extends : ["eslint:recommended", "plugin:react/recommended", "plugin:cypress/recommended", "next"], + globals : { + Atomics : "readonly", + SharedArrayBuffer : "readonly", + globalThis : "readonly", }, - "parser" : "@babel/eslint-parser", - "parserOptions" : { - "ecmaFeatures" : { - "jsx" : true + parser : "@babel/eslint-parser", + parserOptions : { + ecmaFeatures : { + jsx : true, }, - "ecmaVersion" : 2018, - "sourceType" : "module" + ecmaVersion : 2018, + sourceType : "module", }, - "plugins" : ["react", "cypress"], - "rules" : { + plugins : ["react", "cypress"], + rules : { "@next/next/no-img-element" : "off", // turn off next img/image warning "react-hooks/rules-of-hooks" : "warn", @@ -43,33 +43,43 @@ module.exports = { "keyword-spacing" : "error", "no-trailing-spaces" : "error", "object-curly-spacing" : ["error", "always"], - "arrow-spacing" : ["error", { - "before" : true, - "after" : true - }], - "key-spacing" : ["error", { - "beforeColon" : true, - "afterColon" : true - }], - "block-spacing" : "error", - "brace-style" : ["error", "1tbs"], - 'indent' : ['error', 2, { - "FunctionExpression" : { - "parameters" : "first" + "arrow-spacing" : [ + "error", + { + before : true, + after : true, }, - "FunctionDeclaration" : { - "parameters" : "first" + ], + "key-spacing" : [ + "error", + { + afterColon : true, + beforeColon : true }, - "MemberExpression" : 1, - "SwitchCase" : 1, - "outerIIFEBody" : 0, - "VariableDeclarator" : { - "var" : 2, - "let" : 2, - "const" : 3 + ], + "block-spacing" : "error", + "brace-style" : ["error", "1tbs"], + indent : [ + "error", + 2, + { + FunctionExpression : { + parameters : "first", + }, + FunctionDeclaration : { + parameters : "first", + }, + MemberExpression : 1, + SwitchCase : 1, + outerIIFEBody : 0, + VariableDeclarator : { + var : 2, + let : 2, + const : 3, + }, + ignoredNodes : ["TemplateLiteral"], }, - ignoredNodes : ['TemplateLiteral'] - }], + ], "react/react-in-jsx-scope" : "off", "no-undef" : "error", "react/jsx-uses-vars" : [2], @@ -78,9 +88,12 @@ module.exports = { "no-unused-vars" : "error", "react/jsx-key" : "warn", "no-dupe-keys" : "error", - "react/jsx-filename-extension" : [1, { - "extensions" : [".js", ".jsx"] - }], - "react/prop-types" : "off" - } + "react/jsx-filename-extension" : [ + 1, + { + extensions : [".js", ".jsx"], + }, + ], + "react/prop-types" : "off", + }, }; \ No newline at end of file diff --git a/ui/assets/icons/AlertIcon.js b/ui/assets/icons/AlertIcon.js new file mode 100644 index 00000000000..06de0f52972 --- /dev/null +++ b/ui/assets/icons/AlertIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +const AlertIcon = ({ height, width, fill, style = {} }) => { + return ( + + + + + ); +}; + +export default AlertIcon; \ No newline at end of file diff --git a/ui/assets/icons/ArchiveIcon.js b/ui/assets/icons/ArchiveIcon.js new file mode 100644 index 00000000000..22f0e9cf160 --- /dev/null +++ b/ui/assets/icons/ArchiveIcon.js @@ -0,0 +1,24 @@ + +import React from "react"; +const ArchiveIcon = ({ height, width, fill, innerFill = "#fff", style = {} }) => { + return ( + + + + + + + + + + + ); +}; + +export default ArchiveIcon; \ No newline at end of file diff --git a/ui/assets/icons/ClearIcon.js b/ui/assets/icons/ClearIcon.js index 13136b60d2f..d12c31cdba4 100644 --- a/ui/assets/icons/ClearIcon.js +++ b/ui/assets/icons/ClearIcon.js @@ -12,4 +12,4 @@ const ClearIcon = (props) => { ) }; -export default ClearIcon; +export default ClearIcon; \ No newline at end of file diff --git a/ui/assets/icons/ContentFilterIcon.js b/ui/assets/icons/ContentFilterIcon.js new file mode 100644 index 00000000000..cabb2cc80a6 --- /dev/null +++ b/ui/assets/icons/ContentFilterIcon.js @@ -0,0 +1,26 @@ +import React from "react"; + +const ContentFilterIcon = (props) => { + return ( + + + + + ); +}; + +export default ContentFilterIcon; + diff --git a/ui/assets/icons/CrossCircleIcon.js b/ui/assets/icons/CrossCircleIcon.js new file mode 100644 index 00000000000..6d24c127bf6 --- /dev/null +++ b/ui/assets/icons/CrossCircleIcon.js @@ -0,0 +1,31 @@ +import React from "react"; + +const CrossCircleIcon = (props) => { + return ( + + + + + + + + + + + ); +}; + +export default CrossCircleIcon; diff --git a/ui/components/Header.js b/ui/components/Header.js index 20bfd6967b3..e5cf557b550 100644 --- a/ui/components/Header.js +++ b/ui/components/Header.js @@ -14,7 +14,7 @@ import NoSsr from '@material-ui/core/NoSsr'; import Link from 'next/link'; import SettingsIcon from '@material-ui/icons/Settings'; import Chip from '@material-ui/core/Chip'; -import MesheryNotification from './MesheryNotification'; +import MesheryNotification from './NotificationCenter'; import User from './User'; import subscribeBrokerStatusEvents from "./graphql/subscriptions/BrokerStatusSubscription" import Slide from '@material-ui/core/Slide'; @@ -675,4 +675,4 @@ const mapDispatchToProps = (dispatch) => ({ }); -export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(withNotify(Header))); +export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(withNotify(Header))); \ No newline at end of file diff --git a/ui/components/MesheryNotification.js b/ui/components/MesheryNotification.js deleted file mode 100644 index 07db9d36e23..00000000000 --- a/ui/components/MesheryNotification.js +++ /dev/null @@ -1,494 +0,0 @@ -import React from "react"; -import IconButton from '@material-ui/core/IconButton'; -import { connect } from 'react-redux'; -import NoSsr from '@material-ui/core/NoSsr'; -import { - Badge, - Drawer, - Tooltip, - Divider, - Typography, - Tabs, - Tab, - ClickAwayListener, -} from '@material-ui/core'; -import BellIcon from '@material-ui/icons/Notifications'; -import ClearIcon from "../assets/icons/ClearIcon"; -import ErrorIcon from '@material-ui/icons/Error'; -import { withStyles } from '@material-ui/core/styles'; -import amber from '@material-ui/core/colors/amber'; -import { EVENT_TYPES, NOTIFICATION_STATUS, SERVER_EVENT_TYPES } from '../lib/event-types'; -import Notification from "./NotificationCenter/Notification" -import dataFetch from '../lib/data-fetch'; -import { bindActionCreators } from "redux"; -import { loadEventsFromPersistence, toggleNotificationCenter, updateEvents } from "../lib/store"; -import { iconMedium } from "../css/icons.styles"; -import { cursorNotAllowed } from "../css/disableComponent.styles"; -import { v4 } from "uuid"; -import moment from "moment"; -import { withNotify } from "../utils/hooks/useNotification"; - -const styles = (theme) => ({ - sidelist : { width : "35rem", }, - notificationButton : { height : '100%', }, - notificationDrawer : { - backgroundColor : theme.palette.secondary.sideBar, - display : 'flex', - flexDirection : 'column', - justifyContent : 'space-between' - }, - listTop : { - display : 'grid', - alignItems : 'center', - gridTemplateColumns : "2fr 6fr 2fr", - paddingTop : theme.spacing(2), - paddingLeft : theme.spacing(1), - paddingRight : theme.spacing(1), - paddingBottom : theme.spacing(2), - }, - notificationTitle : { textAlign : 'left', }, - notifSelector : { display : 'flex', }, - icon : { fontSize : 20, }, - iconVariant : { - opacity : 0.9, - marginRight : theme.spacing(1), - marginTop : theme.spacing(1) * 3 / 4, - }, - error : { backgroundColor : theme.palette.error.dark, }, - info : { backgroundColor : theme.palette.primary.dark, }, - warning : { backgroundColor : amber[700], }, - message : { - display : 'flex', - // alignItems: 'center', - }, - clearAllButton : { - display : 'flex', - justifyContent : 'flex-end' - }, - drawerButton : { - padding : '0.45rem', - margin : '0.2rem', - backgroundColor : theme.palette.secondary.dark, - color : '#FFFFFF', - "&:hover" : { - backgroundColor : '#FFFFFF', - color : theme.palette.secondary.dark - } - }, - fullView : { - right : 0, - transition : '0.3s ease-in-out !important' - }, - peekView : { - right : "-32.1rem", - transition : '0.3s ease-in-out !important' - }, - tabs : { - - "&.MuiTabs-flexContainer" : { - display : "flex", - justifyContent : "space-around", - }, - "& .MuiTabs-indicator" : { - backgroundColor : theme.palette.type === 'dark' ? "#00B39F" : theme.palette.primary, - }, - }, - tab : { - padding : "0px", - margin : "0px", - "&.Mui-selected" : { - color : theme.palette.type === 'dark' ? "#00B39F" : theme.palette.primary, - }, - }, - - notification : { - margin : theme.spacing(0.5, 1) - } -}); - - - - -export const NOTIFICATION_FILTERS = { - ALL : "all", - ERROR : EVENT_TYPES.ERROR.type , - SUCCESS : EVENT_TYPES.SUCCESS.type , - WARNING : EVENT_TYPES.WARNING.type, - HISTORY : "history" , -} - -/** - * getNotifications filters the notifications based on the - * given type and returns an array of filtered notifications - * @param {{ - * event_type: number, - * summary: string, - * detail: string, - * operation_id: string - * }[]} events - * - * @returns {{ - * event_type: number, - * summary: string, - * details: string, - * operation_id: string - * }[]} - */ -function getNotifications(events, filter) { - if (!Array.isArray(events)) return []; - if (filter == NOTIFICATION_FILTERS.HISTORY) return events.filter(ev => ev.status === NOTIFICATION_STATUS.VIEWED) - - if (filter == NOTIFICATION_FILTERS.ALL) return events.filter(ev => ev.status !== NOTIFICATION_STATUS.VIEWED) - - if (filter === NOTIFICATION_FILTERS.SUCCESS) { - return events.filter(ev => { - const ev_type = getEventType(ev).type - return (ev_type == (EVENT_TYPES.SUCCESS.type || ev_type == EVENT_TYPES.INFO.type) && ev.status !== NOTIFICATION_STATUS.VIEWED ) - }) - } - return events.filter(ev => getEventType(ev).type == filter && ev.status !== NOTIFICATION_STATUS.VIEWED ) - -} - -/** - * - * @param {{ - * event_type: number, - * summary: string, - * details: string, - * operation_id: string - * }[]} events - */ -function getNotificationCount(events) { - if (!Array.isArray(events)) return 0; - - const errorEventCount = getNotifications(events,NOTIFICATION_FILTERS.ERROR).length; - const totalEventsCount = getNotifications(events,NOTIFICATION_FILTERS.ALL).length; - return errorEventCount || totalEventsCount; -} - - -const getEventType = (event) => { - // checks if an event_type is as cardinal (0 , 1 ,2 ) or as a event_type object - // return the event_type object - let eventVariant = event.event_type - eventVariant = typeof eventVariant == "number" ? SERVER_EVENT_TYPES[eventVariant] : eventVariant - return eventVariant ? eventVariant : EVENT_TYPES.INFO -} - - -/** - * NotificationIcon is a wrapper react component for rendering - * icons based on the "type" prop - * @param {{ type: string,className: string }} props - */ -function NotificationIcon({ type, className }) { - if (type === "error") return - - return -} - -const notificationBadgeTooltipMessage = (events) => { - const total_unread = getNotifications(events,NOTIFICATION_FILTERS.ALL).length - if (total_unread) { - return `${total_unread} new notifications` - } - return `No new notifications` -} - -const TabLabel = ({ filterType,events }) => { - - const notifCount = getNotifications(events,filterType).length - return ( -
- {notifCount ? `${filterType} (${notifCount})` : filterType } -
- ) -} - -//TODO: Convert To functional Compoent -class MesheryNotification extends React.Component { - state = { - open : false, - dialogShow : false, - displayEventType : NOTIFICATION_FILTERS.ALL, - tabValue : 0, - anchorEl : false, - } - - handleToggle = () => { - this.props.toggleOpen() - }; - - handleClose = () => { - if (! this.props.showFullNotificationCenter) { - return - } - this.setState({ anchorEl : false }); - this.props.toggleOpen() - - } - - /** - * notificationDispatcher dispatches the notifications - * @param {number} type type of the event - * @param {string} message message to be displayed - */ - - // const {notify} = useNotification() - - componentDidMount() { - this.startEventStream(); - } - - componentDidUpdate() { - if (this.props.user.get("user_id") && this.props.events.length == 0) { - this.props.loadEventsFromPersistence() - } - } - - async startEventStream() { - this.closeEventStream(); - this.eventStream = new EventSource('/api/events'); - this.eventStream.onmessage = this.handleEvents(); - this.eventStream.onerror = this.handleError(); - } - - - handleEvents() { - const self = this - return (e) => { - const data = JSON.parse(e.data); - const event = { - ...data, - status : NOTIFICATION_STATUS.NEW, - event_type : getEventType(data), - timestamp : data.timestamp || moment.utc().valueOf() , - id : data.id || v4() , - } - self.props.notify({ - message : event.summary , - event_type : event.event_type, - details : event.details, - customEvent : event - }) - } - } - - handleError() { - const self = this; - return () => { - self.closeEventStream(); - // check if server is available - dataFetch('/api/user', { credentials : 'same-origin' }, () => { - // attempting to reestablish connection - // setTimeout(() => function() { - self.startEventStream(); - // }, 2000); - }, () => { - // do nothing here - }); - }; - } - - closeEventStream() { - if (this.eventStream && this.eventStream.close) { - this.eventStream.close(); - } - } - - deleteEvent = (id) => { - const { events, updateEvents } = this.props; - const newEvents = events.filter(ev => ev.id !== id) - updateEvents({ events : newEvents }) - this.setState({ dialogShow : false }); - } - - handleDialogClose = () => { - this.setState({ dialogShow : false }); - }; - - handleClearAllNotifications() { - const self = this; - const { updateEvents } = this.props; - return () => { - updateEvents({ events : [] }) - self.handleClose(); - }; - } - - handleNotifFiltering(type) { - return () => { - this.setState({ displayEventType : type }) - } - } - - handleTabChange = (_event, newTabValue) => { - this.setState({ tabValue : newTabValue }) - } - - handleBellButtonClick = () => { - this.setState({ - tabValue : 0, - displayEventType : '*' - }) - } - - markAsRead = (event) => { - const events = this.props.events.filter(ev => ev.id !== event.id) - events.push({ - ...event, - status : NOTIFICATION_STATUS.VIEWED - }) - this.props.updateEvents({ - events - }) - } - - - render() { - const { classes, events ,showFullNotificationCenter } = this.props; - const { anchorEl, show } = this.state; - let open = Boolean(anchorEl); - if (showFullNotificationCenter) { - open = showFullNotificationCenter; - } - - const newErrors = getNotifications(events,NOTIFICATION_FILTERS.ERROR) - const newNotificationsType = newErrors.length > 0 ? "error" : "default" - - return ( - -
- - { - this.anchorEl = node; - }} - color="inherit" - onClick={this.handleToggle} - - onMouseOver={(e) => { - e.preventDefault(); - this.setState({ anchorEl : true }) - }} - - onMouseLeave={(e) => { - e.preventDefault(); - this.setState({ anchorEl : false }) - }} - > - - - - - -
- - { - if (e.target.className.baseVal !== "" && e.target.className.baseVal !== "MuiSvgIcon-root" && - ((typeof e.target.className === "string")? !e.target.className?.includes("MesheryNotification"): null)) { - this.handleClose(); - } - }}> - -
-
-
-
-
- - - - - -
-
- - Notifications - -
-
- - - - - -
-
- - - - {Object.values(NOTIFICATION_FILTERS).map(filter => - } - className={classes.tab} - onClick={this.handleNotifFiltering(filter)} - style={{ minWidth : "8%" }} /> - )} - - {getNotifications(this.props.events, this.state.displayEventType).map((event) => ( - this.deleteEvent(event.id)} - onMarkAsRead = {() => this.markAsRead(event) } - expand={(this.props.openEventId && this.props.openEventId === ( event.id) ) ? true : false} - /> - ))} -
-
-
-
-
-
- ); - } -} - -const mapDispatchToProps = (dispatch) => ({ - updateEvents : bindActionCreators(updateEvents, dispatch), - toggleOpen : bindActionCreators(toggleNotificationCenter,dispatch), - loadEventsFromPersistence : bindActionCreators(loadEventsFromPersistence,dispatch) -}) - -const mapStateToProps = (state) => { - const events = state.get('events') - return { - user : state.get("user"), - events : events.toJS(), - openEventId : state.get("notificationCenter").get("openEventId"), - showFullNotificationCenter : state.get("notificationCenter").get("showFullNotificationCenter") - }; -}; - -export default withStyles(styles)(connect( - mapStateToProps, - mapDispatchToProps, -)(withNotify(MesheryNotification))); \ No newline at end of file diff --git a/ui/components/NotificationCenter/filter.js b/ui/components/NotificationCenter/filter.js new file mode 100644 index 00000000000..a3573696f31 --- /dev/null +++ b/ui/components/NotificationCenter/filter.js @@ -0,0 +1,459 @@ +import { + ClickAwayListener, + Divider, + Fade, + IconButton, + InputAdornment, + List, + Popper, + TextField, + Typography, + alpha, + makeStyles, + useTheme, +} from "@material-ui/core"; +import ContentFilterIcon from "../../assets/icons/ContentFilterIcon"; +import { useReducer, useRef, useState } from "react"; +import CrossCircleIcon from "../../assets/icons/CrossCircleIcon"; +import clsx from "clsx"; + +const useStyles = makeStyles((theme) => ({ + root : { + position : "relative", + backgroundColor : theme.palette.secondary.elevatedComponents, + }, + input : { + width : "100%", + "& .MuiOutlinedInput-root" : { + borderRadius : "6px", + backgroundColor : theme.palette.secondary.searchBackground, + "& fieldset" : { + borderRadius : "6px", + border : `2px solid ${theme.palette.secondary.searchBorder}`, + }, + }, + }, + + dropDown : { + backgroundColor : theme.palette.secondary.searchBackground, + borderRadius : "6px", + boxShadow : + "0px 2px 4px 0px rgba(0, 0, 0, 0.20), 0px 1px 10px 0px rgba(0, 0, 0, 0.12), 0px 4px 5px 0px rgba(0, 0, 0, 0.14)", + border : `2px solid ${theme.palette.secondary.searchBorder}`, + marginTop : "0.2rem", + }, +})); + +const useFilterStyles = makeStyles((theme) => ({ + item : { + fontFamily : "Qanelas Soft, sans-serif", + display : "flex", + gap : "0.3rem", + margin : "0.3rem", + padding : "0.3rem", + paddingInline : "1rem", + borderRadius : "6px", + cursor : "pointer", + "&:hover" : { + backgroundColor : alpha(theme.palette.secondary.link2, 0.25), + }, + }, + + label : { + fontWeight : 500, + color : theme.palette.secondary.icon, + }, + description : { + fontWeight : 400, + color : theme.palette.secondary.number, + }, +})); + +const FILTERS = { + SEVERITY : { + value : "severity", + label : "Severity", + description : "Filter by severity", + values : ["info", "warning", "error"], + }, + + TYPE : { + value : "type", + label : "Type", + description : "Filter by type", + }, + + AUTHOR : { + value : "author", + label : "Author", + description : "Filter by any user or system", + }, + + CATEGORY : { + value : "category", + label : "Category", + description : "Filter by category", + values : ["meshsync", "meshery"], + }, +}; + +const FILTERING_STATE = { + IDLE : "idle", + SELECTING_FILTER : "selecting_filter", + SELECTING_VALUE : "selecting_value", +}; + +const FILTER_EVENTS = { + START : "start", + SELECT : "select_filter", + SELECT_FILTER : "select_filter", + INPUT_CHANGE : "input_change", + SELECT_FILTER_VALUE : "select_filter_value", + CLEAR : "clear", + EXIT : "exit", +}; + +const Delimiter = { + FILTER : " ", + FILTER_VALUE : ":", +}; + +const commonReducer = (stateMachine, action) => { + const { context } = stateMachine; + switch (action.type) { + case FILTER_EVENTS.CLEAR: + return { + state : FILTERING_STATE.SELECTING_FILTER, + context : { + ...context, + value : "", + prevValue : [""], + }, + }; + + case FILTER_EVENTS.EXIT: + return { + state : FILTERING_STATE.IDLE, + context : { + ...context, + value : "", + prevValue : [""], + }, + }; + + default: + return stateMachine; + } +}; + +const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { + const { state, context } = stateMachine; + const nextDelimiter = nextState == FILTERING_STATE.SELECTING_FILTER ? Delimiter.FILTER : Delimiter.FILTER_VALUE; + const prevDelimiter = nextDelimiter == Delimiter.FILTER_VALUE ? Delimiter.FILTER : Delimiter.FILTER_VALUE; + const prevState = nextState; // same beccuase the prevState is the same as the nextState ( as we have only two states) + switch (action.type) { + // Select a filter and move to start entring its value + case FILTER_EVENTS.SELECT: { + const newValue = nextValue(context.prevValue.at(-1), action.payload.value); // ":" is used to separate the filter and its value) + return { + state : nextState, + context : { + ...context, + value : newValue + nextDelimiter, + prevValue : [...context.prevValue, newValue], + }, + }; + } + //" " is used to separate multiple filters + case FILTER_EVENTS.INPUT_CHANGE: + // prevent transition when the the filter/value is empty + if (action.payload.value.endsWith(nextDelimiter) && context.value.endsWith(prevDelimiter)) { + return stateMachine; + } + + // prevent adding multiple delimeters together + if (action.payload.value.endsWith(prevDelimiter) && context.value.endsWith(prevDelimiter)) { + return stateMachine; + } + + if (action.payload.value == context.prevValue.at(-1)) { + return { + state : prevState, + context : { + ...context, + prevValue : context.prevValue.slice(0, -1), + value : action.payload.value, + }, + }; + } + + if (action.payload.value.endsWith(nextDelimiter)) { + const newValue = action.payload.value; + return { + state : nextState, + context : { + ...context, + value : action.payload.value, + prevValue : [...context.prevValue, newValue.slice(0, -1)], + }, + }; + } + + return { + state, // stay in the same state + context : { + ...context, + value : action.payload.value, + }, + }; + default: + return commonReducer(stateMachine, action); + } +}; + +const filterReducer = (stateMachine, action) => { + const { state } = stateMachine; + switch (state) { + // Initial State + case FILTERING_STATE.IDLE: + switch (action.type) { + // Start the filter process + case "START": + return { + ...stateMachine, + state : FILTERING_STATE.SELECTING_FILTER, + }; + default: + return stateMachine; + } + + case FILTERING_STATE.SELECTING_FILTER: + // return filterSelectionReducer(stateMachine, action); + return filterSelectionReducer( + stateMachine, + action, + FILTERING_STATE.SELECTING_VALUE, + (prevValue, value) => prevValue + Delimiter.FILTER + value + ); + + case FILTERING_STATE.SELECTING_VALUE: + return filterSelectionReducer( + stateMachine, + action, + FILTERING_STATE.SELECTING_FILTER, + (prevValue, value) => prevValue + Delimiter.FILTER_VALUE + value + ); + + // runs for all states + default: + return stateMachine; + } +}; + +const getCurrentFilterAndValue = (filteringState) => { + const { context } = filteringState; + const currentFilterValue = context.value.split(Delimiter.FILTER).at(-1); + const currentFilter = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[0] || ""; + const currentValue = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[1] || ""; + return { + filter : currentFilter, + value : currentValue, + }; +}; + +const Filters = ({ filterStateMachine, dispatchFilterMachine }) => { + const classes = useFilterStyles(); + const selectFilter = (filter) => { + dispatchFilterMachine({ + type : FILTER_EVENTS.SELECT, + payload : { + value : filter, + }, + }); + }; + + const { filter : currentFilter } = getCurrentFilterAndValue(filterStateMachine); + const matchingFilters = currentFilter + ? Object.values(FILTERS).filter((filter) => filter.value.startsWith(currentFilter)) + : Object.values(FILTERS); + return ( + + {matchingFilters.length == 0 && ( +
+ + Sorry we dont currently support this filter + +
+ )} + {matchingFilters.map((filter) => { + return ( + <> +
selectFilter(filter.value)}> + + {filter.value}: + + + {filter.description} + +
+ + + ); + })} +
+ ); +}; + +const FilterValueSuggestions = ({ filterStateMachine, dispatchFilterMachine }) => { + const classes = useFilterStyles(); + + const selectValue = (value) => { + dispatchFilterMachine({ + type : FILTER_EVENTS.SELECT, + payload : { + value, + }, + }); + }; + const { filter, value } = getCurrentFilterAndValue(filterStateMachine); + const currentFilter = Object.values(FILTERS).find((f) => f.value == filter); + const suggestions = currentFilter?.values?.filter((v) => v.startsWith(value)) || []; + + return ( + + {suggestions.length == 0 && ( +
+ + No results available + +
+ )} + {suggestions.map((value) => { + return ( + <> +
selectValue(value)}> + + {value} + +
+ + + ); + })} +
+ ); +}; + +const Filter = () => { + const theme = useTheme(); + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(null); + const isPopperOpen = Boolean(anchorEl); + const inputFieldRef = useRef(null); + const [filteringState, dispatch] = useReducer(filterReducer, { + context : { + value : "", + prevValue : [""], + }, + state : FILTERING_STATE.IDLE, + }); + + const handleFilterChange = (e) => { + if (e.target.value === "") { + return dispatch({ + type : FILTER_EVENTS.CLEAR, + }); + } + + return dispatch({ + type : FILTER_EVENTS.INPUT_CHANGE, + payload : { + value : e.target.value, + }, + }); + }; + + const handleClear = () => { + dispatch({ + type : FILTER_EVENTS.EXIT, + }); + }; + + const handleFocus = (e) => { + setAnchorEl(e.currentTarget); + dispatch({ type : "START" }); + }; + + const handleClickAway = (e) => { + if (inputFieldRef.current.contains(e.target)) { + return; + } + + setAnchorEl(null); + }; + + return ( +
+ + {" "} + {" "} + + ), + endAdornment : ( + + + {filteringState.state !== FILTERING_STATE.IDLE && ( + + )} + + + ), + }} + /> + + + {({ TransitionProps }) => { + return ( + + +
+ {filteringState.state == FILTERING_STATE.SELECTING_FILTER && ( + + )} + {filteringState.state == FILTERING_STATE.SELECTING_VALUE && ( + + )} +
+
+
+ ); + }} +
+
+ ); +}; + +export default Filter; diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js new file mode 100644 index 00000000000..5f9f19b5526 --- /dev/null +++ b/ui/components/NotificationCenter/index.js @@ -0,0 +1,259 @@ +import React, { useState } from "react"; +import IconButton from "@material-ui/core/IconButton"; +import { connect } from "react-redux"; +import NoSsr from "@material-ui/core/NoSsr"; +import { Drawer, Tooltip, Divider, ClickAwayListener, Box, Typography, makeStyles, alpha } from "@material-ui/core"; +import Filter from "./filter"; +import BellIcon from "../../assets/icons/BellIcon.js" +import ErrorIcon from "../../assets/icons/ErrorIcon.js" +import { loadEventsFromPersistence, toggleNotificationCenter, updateEvents } from "../../lib/store"; +import { iconMedium } from "../../css/icons.styles"; +import { bindActionCreators } from "redux"; +import { NOTIFICATIONCOLORS } from "../../themes"; +import AlertIcon from "../../assets/icons/AlertIcon"; +import ArchiveIcon from "../../assets/icons/ArchiveIcon"; + +const useStyles = makeStyles((theme) => ({ + sidelist : { width : "45rem" }, + notificationButton : { height : "100%" }, + notificationDrawer : { + backgroundColor : theme.palette.secondary.sideBar, + display : "flex", + flexDirection : "column", + justifyContent : "space-between", + }, + drawerButton : { + padding : "0.45rem", + margin : "0.2rem", + backgroundColor : theme.palette.secondary.dark, + color : "#FFFFFF", + "&:hover" : { + backgroundColor : "#FFFFFF", + color : theme.palette.secondary.dark, + }, + }, + fullView : { + right : 0, + transition : "0.3s ease-in-out !important", + }, + peekView : { + right : "-42.1rem", + transition : "0.3s ease-in-out !important", + }, + + container : { + padding : "20px" + }, + header : { + marginBottom : "20px", + display : "flex", + gap : "0.5rem", + justifyContent : "space-between", + alignItems : "center", + }, + title : { + display : "flex", + alignItems : "center", + gap : "0.5rem", + }, + titleBellIcon : { + width : "36px", + height : "36px", + borderRadius : "100%", + backgroundColor : "black", + display : "flex", + padding : "0.2rem", + justifyContent : "center", + alignItems : "center" + }, + severityChip : { + borderRadius : "4px", + display : "flex", + gap : "4px", + // alignItems: "center", + padding : "4px 12px", + fontSize : "16px", + }, + + severityChips : { + display : "flex", + gap : "12px", + alignItems : "center", + + }, + notification : { + margin : theme.spacing(0.5, 1), + }, +})); + +const SEVERITY = { + INFO : "info", + ERROR : "error", + WARNING : "warning", + // SUCCESS: "success" +} + +const STATUS = { + ACKNOWLEDGED : "acknowledged" +} +const STATUS_STYLE = { + [STATUS.ACKNOWLEDGED] : { + icon : ArchiveIcon, + color : "#3c494f" // Charcoal + } +} + +const SEVERITY_STYLE = { + [SEVERITY.INFO] : { + icon : ErrorIcon, + color : NOTIFICATIONCOLORS.INFO + }, + [SEVERITY.ERROR] : { + icon : ErrorIcon, + color : NOTIFICATIONCOLORS.ERROR + }, + [SEVERITY.WARNING] : { + icon : AlertIcon, + color : NOTIFICATIONCOLORS.WARNING + }, + +} + + +const NotificationCountChip = ({ classes, notificationStyle, count }) => { + const chipStyles = { + fill : notificationStyle.color, + height : "20px", + width : "20px", + } + count = Number(count).toLocaleString('en', { useGrouping : true }) + return ( +
+ {} + {count} +
+ ) +} + +const Header = () => { + const classes = useStyles() + return ( +
+ +
+
+ +
+ Notifications +
+
+ {Object.values(SEVERITY).map(severity => + + )} + {Object.values(STATUS).map(status => + + )} + +
+
+ +
+ ) +} + + +const MesheryNotification = (props) => { + const [anchorEl, setAnchorEl] = useState(null); + + const handleToggle = () => { + props.toggleOpen(); + }; + + const handleClose = () => { + if (!props.showFullNotificationCenter) { + return; + } + props.toggleOpen(); + setAnchorEl(null); + }; + const classes = useStyles() + const { showFullNotificationCenter } = props; + const open = Boolean(anchorEl) || showFullNotificationCenter; + + return ( + +
+ + { + e.preventDefault(); + setAnchorEl(e.currentTarget); + }} + onMouseLeave={(e) => { + e.preventDefault(); + setAnchorEl(null); + }} + > + + {/* + */} + + +
+ + { + if ( + e.target.className.baseVal !== "" && + e.target.className.baseVal !== "MuiSvgIcon-root" && + (typeof e.target.className === "string" ? !e.target.className?.includes("MesheryNotification") : null) + ) { + handleClose(); + } + }} + > + +
+
+
+
+ + +
+
+
+
+
+
+ ); +}; + +const mapDispatchToProps = (dispatch) => ({ + updateEvents : bindActionCreators(updateEvents, dispatch), + toggleOpen : bindActionCreators(toggleNotificationCenter, dispatch), + loadEventsFromPersistence : bindActionCreators(loadEventsFromPersistence, dispatch), +}); + +const mapStateToProps = (state) => { + const events = state.get("events"); + return { + user : state.get("user"), + events : events.toJS(), + openEventId : state.get("notificationCenter").get("openEventId"), + showFullNotificationCenter : state.get("notificationCenter").get("showFullNotificationCenter"), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(MesheryNotification); diff --git a/ui/themes/app.js b/ui/themes/app.js index 3ee8a72f75d..5e162403255 100644 --- a/ui/themes/app.js +++ b/ui/themes/app.js @@ -58,6 +58,8 @@ export var darkTheme = createTheme({ switcherButtons : '#1e1e1e', honeyComb : '#303030', filterChipBackground : '#222222', + searchBackground : "#294957", + searchBorder : "#396679", tabs : '#202020', modalTabs : '#363636', tabHover : '#212121', @@ -282,6 +284,8 @@ let theme = createTheme({ switcherButtons : '#335c6d', honeyComb : '#F0F0F0', filterChipBackground : '#CCCCCC', + searchBackground : "#fff", + searchBorder : "#CCCCCC", tabs : '#eeeeee87', modalTabs : '#dddddd', tabHover : '#e3e3e3', @@ -527,4 +531,4 @@ export const styles = (theme) => ({ backgroundColor : notificationColors.warning, padding : theme.spacing(2), } -}); +}); \ No newline at end of file From 1df3c8bca75c7707eba8dc94b300c7b06915d634 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Wed, 6 Sep 2023 11:44:20 +0530 Subject: [PATCH 02/44] add colors to theme Signed-off-by: aabidsofi19 --- ui/themes/app.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ui/themes/app.js b/ui/themes/app.js index 5e162403255..2f542ff1713 100644 --- a/ui/themes/app.js +++ b/ui/themes/app.js @@ -3,6 +3,14 @@ import { blueGrey } from '@material-ui/core/colors'; import { iconMedium } from '../css/icons.styles'; const drawerWidth = 256; + +export const Colors = { + darkJungleGreen : "#1E2117", + caribbeanGreen : "#00D3a9", + keppelGreen : "#00B39F", + charcoal : "#3C494F", +} + export var darkTheme = createTheme({ typography : { useNextVariants : true, @@ -73,7 +81,7 @@ export var darkTheme = createTheme({ error : "#F91313", lightError : "#B32700", penColorPrimary : '#E6E6E6', - penColorSecondary : '#E6E6E6' + penColorSecondary : '#E6E6E6', }, }, p : { From db5ea0af9820a566791e44167e9221b041c70cc4 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Tue, 12 Sep 2023 00:00:30 +0530 Subject: [PATCH 03/44] Integrated with Api and store Signed-off-by: aabidsofi19 --- ui/assets/icons/FacebookIcon.js | 14 + ui/assets/icons/LinkedInIcon.js | 7 + ui/assets/icons/ShareIcon.js | 14 + ui/assets/icons/TwitterIcon.js | 8 + .../{Notification.js => NotificationOld.js} | 0 ui/components/NotificationCenter/constants.js | 40 + ui/components/NotificationCenter/filter.js | 278 ++++--- ui/components/NotificationCenter/index.js | 323 +++++---- .../NotificationCenter/notification.js | 242 +++++++ .../notificationCenter.style.js | 76 ++ ui/package-lock.json | 87 ++- ui/package.json | 1 + ui/pages/_app.js | 7 +- ui/store/index.js | 8 + ui/store/slices/events.js | 59 ++ ui/themes/app.js | 685 +++++++++--------- 16 files changed, 1235 insertions(+), 614 deletions(-) create mode 100644 ui/assets/icons/FacebookIcon.js create mode 100644 ui/assets/icons/LinkedInIcon.js create mode 100644 ui/assets/icons/ShareIcon.js create mode 100644 ui/assets/icons/TwitterIcon.js rename ui/components/NotificationCenter/{Notification.js => NotificationOld.js} (100%) create mode 100644 ui/components/NotificationCenter/constants.js create mode 100644 ui/components/NotificationCenter/notification.js create mode 100644 ui/components/NotificationCenter/notificationCenter.style.js create mode 100644 ui/store/index.js create mode 100644 ui/store/slices/events.js diff --git a/ui/assets/icons/FacebookIcon.js b/ui/assets/icons/FacebookIcon.js new file mode 100644 index 00000000000..1ca8ee35a53 --- /dev/null +++ b/ui/assets/icons/FacebookIcon.js @@ -0,0 +1,14 @@ +import React from 'react' +export const FacebookIcon = ({ width, height, fill, style = {} }) => { + return ( + + + + + + + + ) +} + +export default FacebookIcon \ No newline at end of file diff --git a/ui/assets/icons/LinkedInIcon.js b/ui/assets/icons/LinkedInIcon.js new file mode 100644 index 00000000000..78412ce87b8 --- /dev/null +++ b/ui/assets/icons/LinkedInIcon.js @@ -0,0 +1,7 @@ +import React from 'react' +export const LinkedInIcon = ({ width, height, fill, style = {} }) => { + return ( + ) +} + +export default LinkedInIcon \ No newline at end of file diff --git a/ui/assets/icons/ShareIcon.js b/ui/assets/icons/ShareIcon.js new file mode 100644 index 00000000000..228bd5bc9cd --- /dev/null +++ b/ui/assets/icons/ShareIcon.js @@ -0,0 +1,14 @@ +import React from 'react' +export const ShareIcon = ({ width, height, fill, style = {} }) => { + return ( + + + + + + + + ) +} + +export default ShareIcon \ No newline at end of file diff --git a/ui/assets/icons/TwitterIcon.js b/ui/assets/icons/TwitterIcon.js new file mode 100644 index 00000000000..5a6c71935e0 --- /dev/null +++ b/ui/assets/icons/TwitterIcon.js @@ -0,0 +1,8 @@ +import React from 'react' +export const TwitterIcon = ({ width, height, fill, style={} }) => { + return ( + + ) +} + +export default TwitterIcon \ No newline at end of file diff --git a/ui/components/NotificationCenter/Notification.js b/ui/components/NotificationCenter/NotificationOld.js similarity index 100% rename from ui/components/NotificationCenter/Notification.js rename to ui/components/NotificationCenter/NotificationOld.js diff --git a/ui/components/NotificationCenter/constants.js b/ui/components/NotificationCenter/constants.js new file mode 100644 index 00000000000..32af86a4da7 --- /dev/null +++ b/ui/components/NotificationCenter/constants.js @@ -0,0 +1,40 @@ +import { NOTIFICATIONCOLORS } from "../../themes" +import AlertIcon from "../../assets/icons/AlertIcon"; +import ArchiveIcon from "../../assets/icons/ArchiveIcon"; +import ErrorIcon from "../../assets/icons/ErrorIcon.js" +import { Colors } from "../../themes/app"; + +export const SEVERITY = { + INFO: "informational", + ERROR: "error", + WARNING: "warning", + // SUCCESS: "success" +} + +export const STATUS = { + READ : "read", + UNREAD : "unread", +} + +export const STATUS_STYLE = { + [STATUS.UNREAD]: { + icon: ArchiveIcon, + color: Colors.charcoal + } +} + +export const SEVERITY_STYLE = { + [SEVERITY.INFO]: { + icon: ErrorIcon, + color: NOTIFICATIONCOLORS.INFO + }, + [SEVERITY.ERROR]: { + icon: ErrorIcon, + color: NOTIFICATIONCOLORS.ERROR + }, + [SEVERITY.WARNING]: { + icon: AlertIcon, + color: NOTIFICATIONCOLORS.WARNING + }, + +} \ No newline at end of file diff --git a/ui/components/NotificationCenter/filter.js b/ui/components/NotificationCenter/filter.js index a3573696f31..8e2c908ad4e 100644 --- a/ui/components/NotificationCenter/filter.js +++ b/ui/components/NotificationCenter/filter.js @@ -13,131 +13,157 @@ import { useTheme, } from "@material-ui/core"; import ContentFilterIcon from "../../assets/icons/ContentFilterIcon"; -import { useReducer, useRef, useState } from "react"; +import { useEffect, useReducer, useRef, useState } from "react"; import CrossCircleIcon from "../../assets/icons/CrossCircleIcon"; import clsx from "clsx"; +import { SEVERITY } from "./constants"; const useStyles = makeStyles((theme) => ({ - root : { - position : "relative", - backgroundColor : theme.palette.secondary.elevatedComponents, + root: { + position: "relative", + backgroundColor: theme.palette.secondary.elevatedComponents, }, - input : { - width : "100%", - "& .MuiOutlinedInput-root" : { - borderRadius : "6px", - backgroundColor : theme.palette.secondary.searchBackground, - "& fieldset" : { - borderRadius : "6px", - border : `2px solid ${theme.palette.secondary.searchBorder}`, + input: { + width: "100%", + "& .MuiOutlinedInput-root": { + borderRadius: "6px", + backgroundColor: theme.palette.secondary.searchBackground, + "& fieldset": { + borderRadius: "6px", + border: `2px solid ${theme.palette.secondary.searchBorder}`, }, }, }, - dropDown : { - backgroundColor : theme.palette.secondary.searchBackground, - borderRadius : "6px", - boxShadow : + dropDown: { + backgroundColor: theme.palette.secondary.searchBackground, + borderRadius: "6px", + boxShadow: "0px 2px 4px 0px rgba(0, 0, 0, 0.20), 0px 1px 10px 0px rgba(0, 0, 0, 0.12), 0px 4px 5px 0px rgba(0, 0, 0, 0.14)", - border : `2px solid ${theme.palette.secondary.searchBorder}`, - marginTop : "0.2rem", + border: `2px solid ${theme.palette.secondary.searchBorder}`, + marginTop: "0.2rem", }, })); const useFilterStyles = makeStyles((theme) => ({ - item : { - fontFamily : "Qanelas Soft, sans-serif", - display : "flex", - gap : "0.3rem", - margin : "0.3rem", - padding : "0.3rem", - paddingInline : "1rem", - borderRadius : "6px", - cursor : "pointer", - "&:hover" : { - backgroundColor : alpha(theme.palette.secondary.link2, 0.25), + item: { + fontFamily: "Qanelas Soft, sans-serif", + display: "flex", + gap: "0.3rem", + margin: "0.3rem", + padding: "0.3rem", + paddingInline: "1rem", + borderRadius: "6px", + cursor: "pointer", + "&:hover": { + backgroundColor: alpha(theme.palette.secondary.link2, 0.25), }, }, - label : { - fontWeight : 500, - color : theme.palette.secondary.icon, + label: { + fontWeight: 500, + color: theme.palette.secondary.icon, }, - description : { - fontWeight : 400, - color : theme.palette.secondary.number, + description: { + fontWeight: 400, + color: theme.palette.secondary.number, }, })); const FILTERS = { - SEVERITY : { - value : "severity", - label : "Severity", - description : "Filter by severity", - values : ["info", "warning", "error"], + SEVERITY: { + value: "severity", + label: "Severity", + description: "Filter by severity", + values: Object.values(SEVERITY), }, - TYPE : { - value : "type", - label : "Type", - description : "Filter by type", + TYPE: { + value: "type", + label: "Type", + description: "Filter by type", }, - AUTHOR : { - value : "author", - label : "Author", - description : "Filter by any user or system", + AUTHOR: { + value: "author", + label: "Author", + description: "Filter by any user or system", }, - CATEGORY : { - value : "category", - label : "Category", - description : "Filter by category", - values : ["meshsync", "meshery"], + CATEGORY: { + value: "category", + label: "Category", + description: "Filter by category", + values: ["pattern", "connection"], }, }; const FILTERING_STATE = { - IDLE : "idle", - SELECTING_FILTER : "selecting_filter", - SELECTING_VALUE : "selecting_value", + IDLE: "idle", + SELECTING_FILTER: "selecting_filter", + SELECTING_VALUE: "selecting_value", }; const FILTER_EVENTS = { - START : "start", - SELECT : "select_filter", - SELECT_FILTER : "select_filter", - INPUT_CHANGE : "input_change", - SELECT_FILTER_VALUE : "select_filter_value", - CLEAR : "clear", - EXIT : "exit", + START: "start", + SELECT: "select_filter", + SELECT_FILTER: "select_filter", + INPUT_CHANGE: "input_change", + SELECT_FILTER_VALUE: "select_filter_value", + CLEAR: "clear", + EXIT: "exit", }; const Delimiter = { - FILTER : " ", - FILTER_VALUE : ":", + FILTER: " ", + FILTER_VALUE: ":", }; +//return a filter object of form { type : {values} , type2 : {values} } +//from the filter string of form "type:value type2:value2 type:value2" +const getFilters = (filterString) => { + const filters = {}; + const filterValuePairs = filterString.split(Delimiter.FILTER); + filterValuePairs.forEach((filterValuePair) => { + const [filter, value] = filterValuePair.split(Delimiter.FILTER_VALUE); + if (filter && value) { + filters[filter] = filters[filter] || new Set(); + filters[filter].add(value); + } + }); + return filters; +}; + +// return a filter string of form "type:value type2:value2 type:value2" +// from a filter object of form { type : {values} , type2 : {values} } +export const getFilterString = (filters) => { + return Object.entries(filters).reduce((filterString, [filter, values]) => { + return filterString + [...values].map((value) => `${filter}${Delimiter.FILTER_VALUE}${value}`).join(" "); + }, ""); +}; + + + const commonReducer = (stateMachine, action) => { const { context } = stateMachine; switch (action.type) { case FILTER_EVENTS.CLEAR: return { - state : FILTERING_STATE.SELECTING_FILTER, - context : { + state: FILTERING_STATE.SELECTING_FILTER, + context: { ...context, - value : "", - prevValue : [""], + value: "", + prevValue: [""], }, }; case FILTER_EVENTS.EXIT: return { - state : FILTERING_STATE.IDLE, - context : { + state: FILTERING_STATE.IDLE, + context: { ...context, - value : "", - prevValue : [""], + value: "", + prevValue: [""], }, }; @@ -156,11 +182,11 @@ const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { case FILTER_EVENTS.SELECT: { const newValue = nextValue(context.prevValue.at(-1), action.payload.value); // ":" is used to separate the filter and its value) return { - state : nextState, - context : { + state: nextState, + context: { ...context, - value : newValue + nextDelimiter, - prevValue : [...context.prevValue, newValue], + value: newValue + nextDelimiter, + prevValue: [...context.prevValue, newValue], }, }; } @@ -178,11 +204,11 @@ const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { if (action.payload.value == context.prevValue.at(-1)) { return { - state : prevState, - context : { + state: prevState, + context: { ...context, - prevValue : context.prevValue.slice(0, -1), - value : action.payload.value, + prevValue: context.prevValue.slice(0, -1), + value: action.payload.value, }, }; } @@ -190,20 +216,20 @@ const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { if (action.payload.value.endsWith(nextDelimiter)) { const newValue = action.payload.value; return { - state : nextState, - context : { + state: nextState, + context: { ...context, - value : action.payload.value, - prevValue : [...context.prevValue, newValue.slice(0, -1)], + value: action.payload.value, + prevValue: [...context.prevValue, newValue.slice(0, -1)], }, }; } return { state, // stay in the same state - context : { + context: { ...context, - value : action.payload.value, + value: action.payload.value, }, }; default: @@ -221,7 +247,7 @@ const filterReducer = (stateMachine, action) => { case "START": return { ...stateMachine, - state : FILTERING_STATE.SELECTING_FILTER, + state: FILTERING_STATE.SELECTING_FILTER, }; default: return stateMachine; @@ -256,8 +282,8 @@ const getCurrentFilterAndValue = (filteringState) => { const currentFilter = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[0] || ""; const currentValue = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[1] || ""; return { - filter : currentFilter, - value : currentValue, + filter: currentFilter, + value: currentValue, }; }; @@ -265,14 +291,14 @@ const Filters = ({ filterStateMachine, dispatchFilterMachine }) => { const classes = useFilterStyles(); const selectFilter = (filter) => { dispatchFilterMachine({ - type : FILTER_EVENTS.SELECT, - payload : { - value : filter, + type: FILTER_EVENTS.SELECT, + payload: { + value: filter, }, }); }; - const { filter : currentFilter } = getCurrentFilterAndValue(filterStateMachine); + const { filter: currentFilter } = getCurrentFilterAndValue(filterStateMachine); const matchingFilters = currentFilter ? Object.values(FILTERS).filter((filter) => filter.value.startsWith(currentFilter)) : Object.values(FILTERS); @@ -309,8 +335,8 @@ const FilterValueSuggestions = ({ filterStateMachine, dispatchFilterMachine }) = const selectValue = (value) => { dispatchFilterMachine({ - type : FILTER_EVENTS.SELECT, - payload : { + type: FILTER_EVENTS.SELECT, + payload: { value, }, }); @@ -344,44 +370,47 @@ const FilterValueSuggestions = ({ filterStateMachine, dispatchFilterMachine }) = ); }; -const Filter = () => { +const Filter = ({ initialFilter, handleFilter }) => { const theme = useTheme(); + console.log("initialFilter", initialFilter) const classes = useStyles(); const [anchorEl, setAnchorEl] = useState(null); const isPopperOpen = Boolean(anchorEl); const inputFieldRef = useRef(null); const [filteringState, dispatch] = useReducer(filterReducer, { - context : { - value : "", - prevValue : [""], + context: { + value: "", + prevValue: [""], }, - state : FILTERING_STATE.IDLE, + state: FILTERING_STATE.IDLE, }); const handleFilterChange = (e) => { if (e.target.value === "") { return dispatch({ - type : FILTER_EVENTS.CLEAR, + type: FILTER_EVENTS.CLEAR, }); } return dispatch({ - type : FILTER_EVENTS.INPUT_CHANGE, - payload : { - value : e.target.value, + type: FILTER_EVENTS.INPUT_CHANGE, + payload: { + value: e.target.value, }, }); }; const handleClear = () => { dispatch({ - type : FILTER_EVENTS.EXIT, + type: FILTER_EVENTS.EXIT, }); + + handleFilter({}) }; const handleFocus = (e) => { setAnchorEl(e.currentTarget); - dispatch({ type : "START" }); + dispatch({ type: "START" }); }; const handleClickAway = (e) => { @@ -392,9 +421,32 @@ const Filter = () => { setAnchorEl(null); }; + //add enter event listener to the input fieldse + //add esc event listener to the input fields + useEffect(() => { + + if (!inputFieldRef.current) { + return; + } + + const handleKeyDown = (e) => { + if (e.key == "Enter") { + handleFilter(getFilters(e.target.value)) + setAnchorEl(null); + } + } + inputFieldRef?.current?.addEventListener("keydown", handleKeyDown) + return () => { + inputFieldRef?.current?.removeEventListener("keydown", handleKeyDown) + } + }, [inputFieldRef.current]) + + + return ( -
+
{ onChange={handleFilterChange} onFocus={handleFocus} InputProps={{ - startAdornment : ( + startAdornment: ( {" "} {" "} ), - endAdornment : ( + endAdornment: ( {filteringState.state !== FILTERING_STATE.IDLE && ( @@ -426,7 +478,7 @@ const Filter = () => { open={filteringState.state != FILTERING_STATE.IDLE && isPopperOpen} anchorEl={inputFieldRef.current} placement="bottom-start" - style={{ zIndex : 2000 }} + style={{ zIndex: 2000 }} transition className="mui-fixed" > @@ -437,7 +489,7 @@ const Filter = () => {
{filteringState.state == FILTERING_STATE.SELECTING_FILTER && ( @@ -456,4 +508,4 @@ const Filter = () => { ); }; -export default Filter; +export default Filter; \ No newline at end of file diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index 5f9f19b5526..04438b9d3e2 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -1,134 +1,33 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import IconButton from "@material-ui/core/IconButton"; -import { connect } from "react-redux"; +import { Provider, connect, useDispatch, useSelector } from "react-redux"; import NoSsr from "@material-ui/core/NoSsr"; -import { Drawer, Tooltip, Divider, ClickAwayListener, Box, Typography, makeStyles, alpha } from "@material-ui/core"; +import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha } from "@material-ui/core"; import Filter from "./filter"; import BellIcon from "../../assets/icons/BellIcon.js" -import ErrorIcon from "../../assets/icons/ErrorIcon.js" import { loadEventsFromPersistence, toggleNotificationCenter, updateEvents } from "../../lib/store"; import { iconMedium } from "../../css/icons.styles"; import { bindActionCreators } from "redux"; -import { NOTIFICATIONCOLORS } from "../../themes"; -import AlertIcon from "../../assets/icons/AlertIcon"; -import ArchiveIcon from "../../assets/icons/ArchiveIcon"; - -const useStyles = makeStyles((theme) => ({ - sidelist : { width : "45rem" }, - notificationButton : { height : "100%" }, - notificationDrawer : { - backgroundColor : theme.palette.secondary.sideBar, - display : "flex", - flexDirection : "column", - justifyContent : "space-between", - }, - drawerButton : { - padding : "0.45rem", - margin : "0.2rem", - backgroundColor : theme.palette.secondary.dark, - color : "#FFFFFF", - "&:hover" : { - backgroundColor : "#FFFFFF", - color : theme.palette.secondary.dark, - }, - }, - fullView : { - right : 0, - transition : "0.3s ease-in-out !important", - }, - peekView : { - right : "-42.1rem", - transition : "0.3s ease-in-out !important", - }, - - container : { - padding : "20px" - }, - header : { - marginBottom : "20px", - display : "flex", - gap : "0.5rem", - justifyContent : "space-between", - alignItems : "center", - }, - title : { - display : "flex", - alignItems : "center", - gap : "0.5rem", - }, - titleBellIcon : { - width : "36px", - height : "36px", - borderRadius : "100%", - backgroundColor : "black", - display : "flex", - padding : "0.2rem", - justifyContent : "center", - alignItems : "center" - }, - severityChip : { - borderRadius : "4px", - display : "flex", - gap : "4px", - // alignItems: "center", - padding : "4px 12px", - fontSize : "16px", - }, - - severityChips : { - display : "flex", - gap : "12px", - alignItems : "center", - - }, - notification : { - margin : theme.spacing(0.5, 1), - }, -})); - -const SEVERITY = { - INFO : "info", - ERROR : "error", - WARNING : "warning", - // SUCCESS: "success" -} - -const STATUS = { - ACKNOWLEDGED : "acknowledged" -} -const STATUS_STYLE = { - [STATUS.ACKNOWLEDGED] : { - icon : ArchiveIcon, - color : "#3c494f" // Charcoal - } -} +import { SEVERITY, SEVERITY_STYLE, STATUS, STATUS_STYLE } from "./constants"; +import axios from "axios"; +import classNames from "classnames"; +import Notification from "./notification"; +import { store } from "../../store"; +import { useStyles } from "./notificationCenter.style"; +import { clearEvents, setEvents, setEventsSummary } from "../../store/slices/events"; -const SEVERITY_STYLE = { - [SEVERITY.INFO] : { - icon : ErrorIcon, - color : NOTIFICATIONCOLORS.INFO - }, - [SEVERITY.ERROR] : { - icon : ErrorIcon, - color : NOTIFICATIONCOLORS.ERROR - }, - [SEVERITY.WARNING] : { - icon : AlertIcon, - color : NOTIFICATIONCOLORS.WARNING - }, -} const NotificationCountChip = ({ classes, notificationStyle, count }) => { const chipStyles = { - fill : notificationStyle.color, - height : "20px", - width : "20px", + fill: notificationStyle.color, + height: "20px", + width: "20px", } - count = Number(count).toLocaleString('en', { useGrouping : true }) + count = Number(count).toLocaleString('en', { useGrouping: true }) return ( -
+
{} {count}
@@ -136,34 +35,123 @@ const NotificationCountChip = ({ classes, notificationStyle, count }) => { } const Header = () => { + useLoadEventsSummary() + const { count_by_severity_level, total_count } = useSelector((state) => state.events.summary); const classes = useStyles() + const getSeverityCount = (severity) => { + return count_by_severity_level.find((item) => item.severity === severity)?.count || 0 + } + + const archivedCount = count_by_severity_level + .reduce((acc, item) => acc + item.count, 0) - total_count + console.log("count_by_severity_level", count_by_severity_level,total_count) return ( -
- -
-
- -
- Notifications +
+
+
+
-
- {Object.values(SEVERITY).map(severity => - - )} - {Object.values(STATUS).map(status => - - )} + Notifications +
+
+ {Object.values(SEVERITY).map(severity => ( + ) + )} + -
- - +
) } +const useLoadEvents = (filters, page) => { + + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [hasMore, setHasMore] = useState(true) + const events = useSelector((state) => state.events.events); + const dispatch = useDispatch() + useEffect(() => { + setLoading(true) + const parsedFilters = {} + Object.keys(filters).forEach((key) => { + if (filters[key]) { + parsedFilters[key] = JSON.stringify(filters[key], (_key, value) => (value instanceof Set ? [...value] : value)) + } + }) + axios.get(`/api/v2/events`, + { + params: { + ...parsedFilters, + page: page, + page_size: 15 + } + } + ).then(({ data }) => { + if (data.events.length === 0) { + setHasMore(false) + return + } + dispatch(setEvents([...events, ...data.events])) + }).catch((err) => { + setError(err) + }).finally(() => { + setLoading(false) + }) + + }, [page, filters]) + + const reset = () => { + dispatch(clearEvents()) + setHasMore(true) + } + + return { + loading, + error, + hasMore, + reset, + } +} + +const useLoadEventsSummary = () => { + + + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const dispatch = useDispatch() + useEffect(() => { + setLoading(true) + axios.get(`/api/v2/events?page=$1&page_size=15`).then(({ data }) => { + dispatch(setEventsSummary({ + count_by_severity_level: data.count_by_severity_level, + total_count: data.total_count + })) + }).catch((err) => { + setError(err) + }).finally(() => { + setLoading(false) + }) + + }, []) + + return { + loading, + error, + } +} const MesheryNotification = (props) => { const [anchorEl, setAnchorEl] = useState(null); + const [filters, setFilters] = useState({}) + const [page, setPage] = useState(1) + + const events = useSelector((state) => state.events.events); + const loadMore = () => { + setPage(page => page + 1) + } const handleToggle = () => { props.toggleOpen(); @@ -180,6 +168,36 @@ const MesheryNotification = (props) => { const { showFullNotificationCenter } = props; const open = Boolean(anchorEl) || showFullNotificationCenter; + const { loading, hasMore, reset } = useLoadEvents(filters, page) + + const loader = React.useRef(null); + const handleObserver = React.useCallback((entries) => { + const target = entries[0]; + if (target.isIntersecting && !loading && hasMore) { + loadMore(); + } + }, [loading]); + + + useEffect(() => { + const option = { + root: null, + rootMargin: "20px", + threshold: 0 + }; + const observer = new IntersectionObserver(handleObserver, option); + if (loader.current) observer.observe(loader.current); + }, [handleObserver]); + + const handleFilter = (filters) => { + reset() + setFilters(filters) + setPage(1) + } + + const value = useSelector((state) => state.events.value); + console.log("value", value) + return (
@@ -221,16 +239,21 @@ const MesheryNotification = (props) => { variant="persistent" open={open} classes={{ - paper : classes.notificationDrawer, - paperAnchorRight : showFullNotificationCenter ? classes.fullView : classes.peekView, + paper: classes.notificationDrawer, + paperAnchorRight: showFullNotificationCenter ? classes.fullView : classes.peekView, }} >
-
- +
+
+ + {events.map((event) => )} + {loading &&
Loading...
} + {!loading && } +
@@ -241,19 +264,29 @@ const MesheryNotification = (props) => { }; const mapDispatchToProps = (dispatch) => ({ - updateEvents : bindActionCreators(updateEvents, dispatch), - toggleOpen : bindActionCreators(toggleNotificationCenter, dispatch), - loadEventsFromPersistence : bindActionCreators(loadEventsFromPersistence, dispatch), + updateEvents: bindActionCreators(updateEvents, dispatch), + toggleOpen: bindActionCreators(toggleNotificationCenter, dispatch), + loadEventsFromPersistence: bindActionCreators(loadEventsFromPersistence, dispatch), }); const mapStateToProps = (state) => { const events = state.get("events"); return { - user : state.get("user"), - events : events.toJS(), - openEventId : state.get("notificationCenter").get("openEventId"), - showFullNotificationCenter : state.get("notificationCenter").get("showFullNotificationCenter"), + user: state.get("user"), + events: events.toJS(), + openEventId: state.get("notificationCenter").get("openEventId"), + showFullNotificationCenter: state.get("notificationCenter").get("showFullNotificationCenter"), }; }; -export default connect(mapStateToProps, mapDispatchToProps)(MesheryNotification); +export default connect(mapStateToProps, mapDispatchToProps)((props) => { + + return ( + <> + + + + + ) + +}); \ No newline at end of file diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js new file mode 100644 index 00000000000..f131d99dec8 --- /dev/null +++ b/ui/components/NotificationCenter/notification.js @@ -0,0 +1,242 @@ +import * as React from 'react'; +import { Box, Button, Collapse, Divider, Grid, IconButton, Popover, Typography, alpha, useTheme } from "@material-ui/core" +import { makeStyles } from "@material-ui/core" +import { SEVERITY_STYLE } from "./constants" +import { iconLarge, iconMedium } from "../../css/icons.styles" +import { MoreVert } from "@material-ui/icons" +import { Avatar } from "@mui/material" +import FacebookIcon from "../../assets/icons/FacebookIcon" +import LinkedInIcon from "../../assets/icons/LinkedInIcon" +import TwitterIcon from "../../assets/icons/TwitterIcon" +import ShareIcon from "../../assets/icons/ShareIcon" +import DeleteIcon from "../../assets/icons/DeleteIcon" +import moment from 'moment'; + + +const useStyles = makeStyles(() => ({ + root: (props) => ({ + width: "100%", + borderRadius: "3px", + border: `1px solid ${props.notificationColor}`, + marginBlock: "8px", + }), + + summary: (props) => ({ + paddingBlock: "8px", + cursor: "pointer", + backgroundColor: alpha(props.notificationColor, 0.20), + }), + + gridItem: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + + message: { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%", + }, + expanded: { + paddingBlock: "12px", + }, + actorAvatar: { + display: "flex", + justifyContent: "center", + alignItems: "start", + }, + + descriptionHeading: { + fontSize: "16px", + fontWeight: "bolder !important", + textTransform: "uppercase", + }, + + +})) + +const useMenuStyles = makeStyles((theme) => { + return { + paper: { + color: theme.palette.secondary.iconMain, + boxShadow: theme.shadows[4], + borderRadius: "3px", + paddingInline: "0.5rem", + paddingBlock: "0.25rem", + width: "200px", + }, + + list: { + padding: "0.5rem", + display: "flex", + flexDirection: "column", + gridGap: "0.5rem", + marginBlock: "0.5rem", + borderRadius: "4px", + backgroundColor: theme.palette.secondary.honeyComb, + }, + + listItem: { + display: "flex", + gridGap: "0.5rem", + alignItems: "center", + // justifyContent: "center", + }, + + } +}) + + +const formatTimestamp = (utcTimestamp) => { + // const curretUtcTimestamp = moment.utc().valueOf() + + // const timediff = currentUtcTimestamp - utcTimestamp + // if (timediff >= 24 * 60 * 60 * 1000) { + return moment(utcTimestamp).local().format('MMM DD, YYYY') + // } + // return moment(utcTimestamp).fromNow() +} + +function BasicMenu() { + + const classes = useMenuStyles() + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const theme = useTheme() + return ( +
+ + + + + +
+ + + Share + + + + + + + + + + + + +
+ +
+ +
+
+
+
+ ); +} + +export const Notification = ({ event }) => { + const severityStyles = SEVERITY_STYLE[event.severity] + const classes = useStyles({ + notificationColor: severityStyles.color + }) + + const [expanded, setExpanded] = React.useState(false) + const handleExpandClick = (e) => { + e.stopPropagation() + setExpanded(!expanded); + }; + + + return ( +
+ + + + + + {event.description} + + + {formatTimestamp(event.update_at)} + + + + + + + + + S + + + +
+ + Details + + + {event.description} + +
+ + + + Probable Cause + +
    + {[1, 2, 3].map((i) =>
  1. + Error Removing Cpx from Kubernetes Context id 23959 . +
  2. )} +
+
+ + + Suggested Remediation + +
    + {[1, 2, 3].map((i) =>
  1. + Error Removing Cpx from Kubernetes Context id 23959 . +
  2. )} +
+
+
+
+
+
+
+
+ ) + +} + +export default Notification \ No newline at end of file diff --git a/ui/components/NotificationCenter/notificationCenter.style.js b/ui/components/NotificationCenter/notificationCenter.style.js new file mode 100644 index 00000000000..6003e25f0ba --- /dev/null +++ b/ui/components/NotificationCenter/notificationCenter.style.js @@ -0,0 +1,76 @@ +import { makeStyles } from "@material-ui/core" + +export const useStyles = makeStyles((theme) => ({ + sidelist: { width: "45rem" }, + notificationButton: { height: "100%" }, + notificationDrawer: { + backgroundColor: theme.palette.secondary.drawer, + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + }, + drawerButton: { + padding: "0.45rem", + margin: "0.2rem", + backgroundColor: theme.palette.secondary.dark, + color: "#FFFFFF", + "&:hover": { + backgroundColor: "#FFFFFF", + color: theme.palette.secondary.dark, + }, + }, + fullView: { + right: 0, + transition: "0.3s ease-in-out !important", + }, + peekView: { + right: "-42.1rem", + transition: "0.3s ease-in-out !important", + }, + + container: { + padding: "20px" + }, + header: { + display: "flex", + gap: "0.5rem", + justifyContent: "space-between", + alignItems: "center", + background: theme.palette.secondary.headerColor, + }, + title: { + display: "flex", + alignItems: "center", + gap: "0.5rem", + }, + titleBellIcon: { + width: "36px", + height: "36px", + borderRadius: "100%", + backgroundColor: "black", + display: "flex", + padding: "0.2rem", + justifyContent: "center", + alignItems: "center" + }, + severityChip: { + borderRadius: "4px", + display: "flex", + gap: "4px", + // alignItems: "center", + padding: "4px 12px", + fontSize: "16px", + }, + + severityChips: { + display: "flex", + gap: "12px", + alignItems: "center", + + }, + notification: { + margin: theme.spacing(0.5, 1), + }, +})); + + diff --git a/ui/package-lock.json b/ui/package-lock.json index 5e374f9702f..dee1eae519a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -30,6 +30,7 @@ "@open-policy-agent/opa-wasm": "^1.8.0", "@paciolan/remote-component": "^2.13.0", "@redux-devtools/extension": "^3.2.3", + "@reduxjs/toolkit": "^1.9.5", "@rjsf/core": "^5.8.1", "@rjsf/material-ui": "^5.8.2", "@rjsf/utils": "^5.8.1", @@ -50,7 +51,7 @@ "isomorphic-unfetch": "^4.0.2", "js-yaml": "^4.1.0", "jsonlint-mod": "^1.7.6", - "jss": "*", + "jss": "latest", "lodash": "^4.17.21", "minimist": ">=1.2.2", "mochawesome": "^7.1.3", @@ -63,7 +64,7 @@ "path-parse": "^1.0.7", "pluralize": "^8.0.0", "postcss": "^8.4.21", - "prop-types": "*", + "prop-types": "latest", "react": "^17.0.2", "react-big-calendar": "^0.35.0", "react-codemirror2": "^7.2.1", @@ -4632,6 +4633,29 @@ "redux": "^3.1.0 || ^4.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@restart/hooks": { "version": "0.3.27", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", @@ -15929,6 +15953,15 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -22133,17 +22166,17 @@ } }, "node_modules/redux": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", - "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "dependencies": { "@babel/runtime": "^7.9.2" } }, "node_modules/redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", "peerDependencies": { "redux": "^4" } @@ -22387,6 +22420,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -29295,6 +29333,17 @@ "immutable": "^4.0.0" } }, + "@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "requires": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + } + }, "@restart/hooks": { "version": "0.3.27", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", @@ -38000,6 +38049,11 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -42710,17 +42764,17 @@ } }, "redux": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz", - "integrity": "sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "requires": { "@babel/runtime": "^7.9.2" } }, "redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==" + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==" }, "regenerate": { "version": "1.4.2", @@ -42918,6 +42972,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", diff --git a/ui/package.json b/ui/package.json index 091030c98c2..7010ba6d90b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -58,6 +58,7 @@ "@open-policy-agent/opa-wasm": "^1.8.0", "@paciolan/remote-component": "^2.13.0", "@redux-devtools/extension": "^3.2.3", + "@reduxjs/toolkit": "^1.9.5", "@rjsf/core": "^5.8.1", "@rjsf/material-ui": "^5.8.2", "@rjsf/utils": "^5.8.1", diff --git a/ui/pages/_app.js b/ui/pages/_app.js index 9ea761cedaf..a7d3b1e9bc0 100644 --- a/ui/pages/_app.js +++ b/ui/pages/_app.js @@ -53,6 +53,10 @@ import { RelayEnvironmentProvider } from 'react-relay'; import { createRelayEnvironment } from "../lib/relayEnvironment" import "./styles/charts.css" import subscribeEvents from '../components/graphql/subscriptions/EventsSubscription'; +import { store as rtkStore } from '../store'; +import { pushEvent } from '../store/slices/events'; + + if (typeof window !== 'undefined') { require('codemirror/mode/yaml/yaml'); @@ -199,7 +203,8 @@ class MesheryApp extends App { } const eventsSubscription = subscribeEvents(result => { - console.log("event: ", result); + console.log("event received", result); + rtkStore.dispatch(pushEvent(result.event)) }) this.eventsSubscriptionRef.current = eventsSubscription; } diff --git a/ui/store/index.js b/ui/store/index.js new file mode 100644 index 00000000000..b69ea7ff3e9 --- /dev/null +++ b/ui/store/index.js @@ -0,0 +1,8 @@ +import eventsReducer from './slices/events' +import { configureStore } from '@reduxjs/toolkit' + +export const store = configureStore({ + reducer: { + events: eventsReducer, + }, +}) \ No newline at end of file diff --git a/ui/store/slices/events.js b/ui/store/slices/events.js new file mode 100644 index 00000000000..1c723c7e9e0 --- /dev/null +++ b/ui/store/slices/events.js @@ -0,0 +1,59 @@ +import { createSlice } from '@reduxjs/toolkit' +import { SEVERITY } from '../../components/NotificationCenter/constants' + +const initialState = { + events: [], + summary: { + count_by_severity_level: [], + total_count: 0 + } +} + +const defaultEventProperties = { + severity: SEVERITY.INFO, +} + +export const eventsSlice = createSlice({ + name: 'events', + initialState, + reducers: { + + clearEvents: (state) => { + state.events = [] + }, + + setEvents: (state, action) => { + state.events = action.payload || [] + }, + + setEventsSummary: (state, action) => { + state.summary = action.payload || [] + }, + + pushEvent: (state, action) => { + const event = { + ...action.payload, + severity: action.payload?.severity?.trim() || defaultEventProperties.severity, + } + state.events = [event, ...state.events] + //update severity count + const severity = event.severity + const severityIndex = state.summary.count_by_severity_level.findIndex((item) => item.severity === severity) + if (severityIndex === -1) { + state.summary.count_by_severity_level.push({ + severity: severity, + count: 1 + }) + } else { + state.summary.count_by_severity_level[severityIndex].count += 1 + } + state.summary.total_count += 1 + }, + + }, +}) + +// Action creators are generated for each case reducer function +export const { pushEvent, clearEvents, setEvents, setEventsSummary } = eventsSlice.actions + +export default eventsSlice.reducer \ No newline at end of file diff --git a/ui/themes/app.js b/ui/themes/app.js index 2f542ff1713..4b4d45c17b9 100644 --- a/ui/themes/app.js +++ b/ui/themes/app.js @@ -5,198 +5,199 @@ import { iconMedium } from '../css/icons.styles'; const drawerWidth = 256; export const Colors = { - darkJungleGreen : "#1E2117", - caribbeanGreen : "#00D3a9", - keppelGreen : "#00B39F", - charcoal : "#3C494F", + darkJungleGreen: "#1E2117", + caribbeanGreen: "#00D3a9", + keppelGreen: "#00B39F", + charcoal: "#3C494F", } export var darkTheme = createTheme({ - typography : { - useNextVariants : true, - h5 : { - fontWeight : 'bolder', - fontSize : 26, - color : '#FFF', - letterSpacing : 0.5, + typography: { + useNextVariants: true, + fontFamily: ['Qanelas Soft', 'Roboto', 'Helvectica', 'Arial' ,'sans-serif'].join(','), + h5: { + fontWeight: 'bolder', + fontSize: 26, + color: '#FFF', + letterSpacing: 0.5, }, - p : { - color : '#FFF', + p: { + color: '#FFF', }, - h6 : { - color : '#FFF', + h6: { + color: '#FFF', } }, - palette : { - type : "dark", - primary : blueGrey, - secondary : { - main : '#EE5351', - primeColor : '#303030', - dark : '#121212', - titleText : '#FBFBFB', - text : '#FFF', - text2 : '#7494a1', - text3 : '#FFF', - textMain : "#F6F8F8", - titleBackground : "#000", - mainBackground : '#202020', - mainBackground2 : '#303030', - elevatedComponents : '#202020', - elevatedComponents2 : '#303030', - elevatedComponents3 : '#303030', - lightText : 'rgba(255, 255, 255, 0.54)', - icon : 'rgba(255, 255, 255, 0.54)', - icon2 : '#E6E6E6', - iconMain : '#F6F8F8', - disabledIcon : 'rgba(255, 255, 255, 0.26)', - chevron : 'rgb(255, 255, 255, 0.2)', - link : 'rgba(255, 255, 255, 0.7)', - link2 : "#05FFCD", - headerColor : '#202020', - sideBar : '#1A1A1A', - drawer : '#252E31', - drawerHover : '#202020', - img : 'invert(0.8)', - appBar : '#363636', - number : '#eee', - completeInvert : 'invert(1)', - canvas : '#1A1A1A', - brightness : 'brightness(200%)', - switcherButtons : '#1e1e1e', - honeyComb : '#303030', - filterChipBackground : '#222222', - searchBackground : "#294957", - searchBorder : "#396679", - tabs : '#202020', - modalTabs : '#363636', - tabHover : '#212121', - confirmationModal : '#111111', - focused : '#00b39f', - primaryModalText : '#FFF', - default : "#9FAFB6", - success : "#00D3A9", - primary : "#86B2C6", - warning : "#EBC017", - error : "#F91313", - lightError : "#B32700", - penColorPrimary : '#E6E6E6', - penColorSecondary : '#E6E6E6', + palette: { + type: "dark", + primary: blueGrey, + secondary: { + main: '#EE5351', + primeColor: '#303030', + dark: '#121212', + titleText: '#FBFBFB', + text: '#FFF', + text2: '#7494a1', + text3: '#FFF', + textMain: "#F6F8F8", + titleBackground: "#000", + mainBackground: '#202020', + mainBackground2: '#303030', + elevatedComponents: '#202020', + elevatedComponents2: '#303030', + elevatedComponents3: '#303030', + lightText: 'rgba(255, 255, 255, 0.54)', + icon: 'rgba(255, 255, 255, 0.54)', + icon2: '#E6E6E6', + iconMain: '#F6F8F8', + disabledIcon: 'rgba(255, 255, 255, 0.26)', + chevron: 'rgb(255, 255, 255, 0.2)', + link: 'rgba(255, 255, 255, 0.7)', + link2: "#05FFCD", + headerColor: '#202020', + sideBar: '#1A1A1A', + drawer: '#252E31', + drawerHover: '#202020', + img: 'invert(0.8)', + appBar: '#363636', + number: '#eee', + completeInvert: 'invert(1)', + canvas: '#1A1A1A', + brightness: 'brightness(200%)', + switcherButtons: '#1e1e1e', + honeyComb: '#303030', + filterChipBackground: '#222222', + searchBackground: "#294957", + searchBorder: "#396679", + tabs: '#202020', + modalTabs: '#363636', + tabHover: '#212121', + confirmationModal: '#111111', + focused: '#00b39f', + primaryModalText: '#FFF', + default: "#9FAFB6", + success: "#00D3A9", + primary: "#86B2C6", + warning: "#EBC017", + error: "#F91313", + lightError: "#B32700", + penColorPrimary: '#E6E6E6', + penColorSecondary: '#E6E6E6', }, }, - p : { - color : '#FFF', + p: { + color: '#FFF', }, - shape : { borderRadius : 8, }, - breakpoints : { - values : { - xs : 0, - sm : 600, - md : 960, - lg : 1280, - xl : 1920, + shape: { borderRadius: 8, }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 960, + lg: 1280, + xl: 1920, }, }, }); darkTheme = { ...darkTheme, - overrides : { - MuiSvgIcon : { - root : { + overrides: { + MuiSvgIcon: { + root: { ...iconMedium } }, - MuiOutlinedInput : { - root : { - "&:hover $notchedOutline" : { - borderColor : "#00B39F", + MuiOutlinedInput: { + root: { + "&:hover $notchedOutline": { + borderColor: "#00B39F", }, - "&$focused $notchedOutline" : { - borderColor : "#00B39F", + "&$focused $notchedOutline": { + borderColor: "#00B39F", }, }, }, - MuiCheckbox : { - colorPrimary : { - "&$checked" : { - color : "rgba(255, 255, 255, 0.7)", + MuiCheckbox: { + colorPrimary: { + "&$checked": { + color: "rgba(255, 255, 255, 0.7)", } }, }, - MuiDrawer : { paper : { backgroundColor : '#263238', }, }, - MuiFormLabel : { - root : { - "&$focused" : { - color : "#00B39F", + MuiDrawer: { paper: { backgroundColor: '#263238', }, }, + MuiFormLabel: { + root: { + "&$focused": { + color: "#00B39F", }, } }, - MuiButton : { - label : { textTransform : 'initial', }, - contained : { - boxShadow : 'none', - color : "rgba(255, 255, 255, 0.87)", - backgroundColor : "#3C494F", - '&:hover' : { backgroundColor : "#505b61", }, - '&:active' : { boxShadow : 'none', }, + MuiButton: { + label: { textTransform: 'initial', }, + contained: { + boxShadow: 'none', + color: "rgba(255, 255, 255, 0.87)", + backgroundColor: "#3C494F", + '&:hover': { backgroundColor: "#505b61", }, + '&:active': { boxShadow: 'none', }, }, - containedPrimary : { - backgroundColor : "#00B39F", - '&:hover' : { backgroundColor : "#00D3A9", }, + containedPrimary: { + backgroundColor: "#00B39F", + '&:hover': { backgroundColor: "#00D3A9", }, }, }, - MuiToggleButton : { - label : { - textTransform : 'initial', - color : '#00B39F', + MuiToggleButton: { + label: { + textTransform: 'initial', + color: '#00B39F', }, }, - MuiTabs : { - root : { marginLeft : darkTheme.spacing(1), }, - indicator : { - height : 3, - backgroundColor : "#00B39F", - borderTopLeftRadius : 3, - borderTopRightRadius : 3, + MuiTabs: { + root: { marginLeft: darkTheme.spacing(1), }, + indicator: { + height: 3, + backgroundColor: "#00B39F", + borderTopLeftRadius: 3, + borderTopRightRadius: 3, }, }, - MuiTab : { - root : { - textTransform : 'initial', - margin : '0 16px', - minWidth : 0, + MuiTab: { + root: { + textTransform: 'initial', + margin: '0 16px', + minWidth: 0, // [darkTheme.breakpoints.up('md')]: { // minWidth: 0, // }, }, - labelContainer : { - padding : 0, + labelContainer: { + padding: 0, // [darkTheme.breakpoints.up('md')]: { // padding: 0, // }, }, - selected : { - color : "#00B39F !important" + selected: { + color: "#00B39F !important" } }, - MuiPaper : { root : { backgroundColor : '#363636' }, elevation2 : { boxShadow : "0px 4px 0px -2px rgb(0 179 159 / 10%), 0px 4px 0px 0px rgb(0 179 159 / 10%), 0px 2px 0px 0px rgb(0 179 159 / 20%)" } }, - MuiIconButton : { root : { padding : darkTheme.spacing(1), }, colorPrimary : { color : "#FFF" }, }, - MuiTooltip : { tooltip : { borderRadius : 4, }, }, - MuiDivider : { root : { backgroundColor : '#404854', }, }, - MuiListItemText : { primary : { fontWeight : darkTheme.typography.fontWeightMedium, }, }, - MuiListItemIcon : { - root : { - color : 'inherit', - marginRight : 0, - '& svg' : { fontSize : 20, }, - justifyContent : 'center', - minWidth : 0 + MuiPaper: { root: { backgroundColor: '#363636' }, elevation2: { boxShadow: "0px 4px 0px -2px rgb(0 179 159 / 10%), 0px 4px 0px 0px rgb(0 179 159 / 10%), 0px 2px 0px 0px rgb(0 179 159 / 20%)" } }, + MuiIconButton: { root: { padding: darkTheme.spacing(1), }, colorPrimary: { color: "#FFF" }, }, + MuiTooltip: { tooltip: { borderRadius: 4, }, }, + MuiDivider: { root: { backgroundColor: '#404854', }, }, + MuiListItemText: { primary: { fontWeight: darkTheme.typography.fontWeightMedium, }, }, + MuiListItemIcon: { + root: { + color: 'inherit', + marginRight: 0, + '& svg': { fontSize: 20, }, + justifyContent: 'center', + minWidth: 0 }, }, - MuiAvatar : { - root : { - width : 32, - height : 32, + MuiAvatar: { + root: { + width: 32, + height: 32, }, }, // Global scrollbar and body styles @@ -235,164 +236,166 @@ darkTheme = { // }, // }, }, - props : { MuiTab : { disableRipple : true, }, }, - mixins : { ...darkTheme.mixins, }, + props: { MuiTab: { disableRipple: true, }, }, + mixins: { ...darkTheme.mixins, }, }; let theme = createTheme({ - typography : { - useNextVariants : true, - h5 : { - fontWeight : 'bolder', - fontSize : 26, - letterSpacing : 0.5, + typography: { + + fontFamily: ['Qanelas Soft', 'Roboto', 'Helvectica', 'Arial' ,'sans-serif'].join(','), + useNextVariants: true, + h5: { + fontWeight: 'bolder', + fontSize: 26, + letterSpacing: 0.5, }, }, - palette : { - type : "light", + palette: { + type: "light", // primary: { // light: '#cfd8dc', // main: '#607d8b', // dark: '#455a64', // }, - primary : blueGrey, - secondary : { - main : '#EE5351', - primeColor : '#ebeff1', - dark : '#455a64', - titleText : '#7494A1', - text : '#000', - text2 : 'rgba(57, 102, 121, .9)', - text3 : '#333333', - textMain : "#3C494F", - titleBackground : "rgba(57, 102, 121, .1)", - mainBackground : '#396679', - mainBackground2 : '#FFF', - elevatedComponents : '#FFF', - elevatedComponents2 : "#eaeff1", - elevatedComponents3 : '#FFF', - lightText : 'rgba(0, 0, 0, 0.54)', - icon : 'rgba(0, 0, 0, 0.54)', - icon2 : 'gray', - iconMain : '#3C494F', - disabledIcon : 'rgba(0, 0, 0, 0.26)', - chevron : '#FFF', - link : '#000', - link2 : "#00b39F", - headerColor : '#eeeeee', - sideBar : '#FFF', - drawer : '#FFF', - drawerHover : '#f2f5f7', - img : 'none', - appBar : '#FFF', - number : '#607d8b', - completeInvert : 'none', - canvas : '#fff', - brightness : 'none', - switcherButtons : '#335c6d', - honeyComb : '#F0F0F0', - filterChipBackground : '#CCCCCC', - searchBackground : "#fff", - searchBorder : "#CCCCCC", - tabs : '#eeeeee87', - modalTabs : '#dddddd', - tabHover : '#e3e3e3', - confirmationModal : 'rgb(234, 235, 236)', - focused : '#607d8b', - primaryModalText : '#FFF', - default : "#51636B", - success : "#00B39F", - primary : "#477E96", - warning : "#F0A303", - error : "#8F1F00", - lightError : "#8F1F00", - penColorPrimary : '#3C494F', - penColorSecondary : '#677E88' + primary: blueGrey, + secondary: { + main: '#EE5351', + primeColor: '#ebeff1', + dark: '#455a64', + titleText: '#7494A1', + text: '#000', + text2: 'rgba(57, 102, 121, .9)', + text3: '#333333', + textMain: "#3C494F", + titleBackground: "rgba(57, 102, 121, .1)", + mainBackground: '#396679', + mainBackground2: '#FFF', + elevatedComponents: '#FFF', + elevatedComponents2: "#eaeff1", + elevatedComponents3: '#FFF', + lightText: 'rgba(0, 0, 0, 0.54)', + icon: 'rgba(0, 0, 0, 0.54)', + icon2: 'gray', + iconMain: '#3C494F', + disabledIcon: 'rgba(0, 0, 0, 0.26)', + chevron: '#FFF', + link: '#000', + link2: "#00b39F", + headerColor: '#eeeeee', + sideBar: '#FFF', + drawer: '#FFF', + drawerHover: '#f2f5f7', + img: 'none', + appBar: '#FFF', + number: '#607d8b', + completeInvert: 'none', + canvas: '#fff', + brightness: 'none', + switcherButtons: '#335c6d', + honeyComb: '#F0F0F0', + filterChipBackground: '#CCCCCC', + searchBackground: "#fff", + searchBorder: "#CCCCCC", + tabs: '#eeeeee87', + modalTabs: '#dddddd', + tabHover: '#e3e3e3', + confirmationModal: 'rgb(234, 235, 236)', + focused: '#607d8b', + primaryModalText: '#FFF', + default: "#51636B", + success: "#00B39F", + primary: "#477E96", + warning: "#F0A303", + error: "#8F1F00", + lightError: "#8F1F00", + penColorPrimary: '#3C494F', + penColorSecondary: '#677E88' }, }, - shape : { borderRadius : 8, }, - breakpoints : { - values : { - xs : 0, - sm : 600, - md : 960, - lg : 1280, - xl : 1920, + shape: { borderRadius: 8, }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 960, + lg: 1280, + xl: 1920, }, }, }); theme = { ...theme, - overrides : { - MuiSvgIcon : { - root : { + overrides: { + MuiSvgIcon: { + root: { ...iconMedium } }, - MuiDrawer : { paper : { backgroundColor : '#263238', }, }, - MuiButton : { - label : { textTransform : 'initial', }, - contained : { - boxShadow : 'none', - '&:active' : { boxShadow : 'none', }, + MuiDrawer: { paper: { backgroundColor: '#263238', }, }, + MuiButton: { + label: { textTransform: 'initial', }, + contained: { + boxShadow: 'none', + '&:active': { boxShadow: 'none', }, }, }, - MuiToggleButton : { - label : { - textTransform : 'initial', - color : '#607d8b', + MuiToggleButton: { + label: { + textTransform: 'initial', + color: '#607d8b', }, }, - MuiTabs : { - root : { marginLeft : theme.spacing(1), }, - indicator : { - height : 3, - borderTopLeftRadius : 3, - borderTopRightRadius : 3, + MuiTabs: { + root: { marginLeft: theme.spacing(1), }, + indicator: { + height: 3, + borderTopLeftRadius: 3, + borderTopRightRadius: 3, }, }, - MuiTab : { - root : { - textTransform : 'initial', - margin : '0 16px', - minWidth : 0, + MuiTab: { + root: { + textTransform: 'initial', + margin: '0 16px', + minWidth: 0, // [theme.breakpoints.up('md')]: { // minWidth: 0, // }, }, - labelContainer : { - padding : 0, + labelContainer: { + padding: 0, // [theme.breakpoints.up('md')]: { // padding: 0, // }, }, }, - MuiIconButton : { root : { padding : theme.spacing(1), }, colorPrimary : { color : "#607d8b" } }, - MuiTooltip : { tooltip : { borderRadius : 4, }, }, - MuiDivider : { root : { backgroundColor : '#404854', }, }, - MuiListItemText : { primary : { fontWeight : theme.typography.fontWeightMedium, }, }, - MuiListItemIcon : { - root : { - color : 'inherit', - marginRight : 0, - '& svg' : { fontSize : 20, }, - justifyContent : 'center', - minWidth : 0 + MuiIconButton: { root: { padding: theme.spacing(1), }, colorPrimary: { color: "#607d8b" } }, + MuiTooltip: { tooltip: { borderRadius: 4, }, }, + MuiDivider: { root: { backgroundColor: '#404854', }, }, + MuiListItemText: { primary: { fontWeight: theme.typography.fontWeightMedium, }, }, + MuiListItemIcon: { + root: { + color: 'inherit', + marginRight: 0, + '& svg': { fontSize: 20, }, + justifyContent: 'center', + minWidth: 0 }, }, - MuiAvatar : { - root : { - width : 32, - height : 32, + MuiAvatar: { + root: { + width: 32, + height: 32, }, }, // global style for body throughout meshery-ui - MuiCssBaseline : { - "@global" : { - body : { - backgroundColor : "#eaeff1", + MuiCssBaseline: { + "@global": { + body: { + backgroundColor: "#eaeff1", }, }, }, @@ -426,117 +429,117 @@ theme = { // }, // }, }, - props : { MuiTab : { disableRipple : true, }, }, - mixins : { ...theme.mixins, }, + props: { MuiTab: { disableRipple: true, }, }, + mixins: { ...theme.mixins, }, }; export default theme export const notificationColors = { - error : "#F91313", - warning : "#F0A303", - success : "#206D24", - info : "#2196F3", - darkRed : "#B32700" + error: "#F91313", + warning: "#F0A303", + success: "#206D24", + info: "#2196F3", + darkRed: "#B32700" }; export const darkNotificationColors = { - error : "#F91313", - warning : "#F0D053", - success : "#78C57C", - info : "#5FD4FF" + error: "#F91313", + warning: "#F0D053", + success: "#78C57C", + info: "#5FD4FF" }; export const styles = (theme) => ({ - root : { - display : 'flex', - minHeight : '100vh', + root: { + display: 'flex', + minHeight: '100vh', }, - drawer : { - [theme.breakpoints.up('sm')] : { - width : drawerWidth, - flexShrink : 0, + drawer: { + [theme.breakpoints.up('sm')]: { + width: drawerWidth, + flexShrink: 0, }, - transition : theme.transitions.create('width', { - easing : theme.transitions.easing.sharp, - duration : theme.transitions.duration.enteringScreen, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, }), }, - drawerCollapsed : { - [theme.breakpoints.up('sm')] : { width : theme.spacing(8.4) + 1, }, - transition : theme.transitions.create('width', { - easing : theme.transitions.easing.sharp, - duration : theme.transitions.duration.leavingScreen, + drawerCollapsed: { + [theme.breakpoints.up('sm')]: { width: theme.spacing(8.4) + 1, }, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, }), - overflowX : 'hidden', + overflowX: 'hidden', }, - appContent : { - flex : 1, - display : 'flex', - flexDirection : 'column', + appContent: { + flex: 1, + display: 'flex', + flexDirection: 'column', }, - mainContent : { - flex : 1, - padding : '48px 36px 24px', + mainContent: { + flex: 1, + padding: '48px 36px 24px', }, - footer : { - backgroundColor : '#fff', - padding : theme.spacing(2), + footer: { + backgroundColor: '#fff', + padding: theme.spacing(2), }, - footerDark : { - backgroundColor : '#202020', - padding : theme.spacing(2), + footerDark: { + backgroundColor: '#202020', + padding: theme.spacing(2), }, - footerText : { - cursor : 'pointer', - display : 'inline', - verticalAlign : 'middle', + footerText: { + cursor: 'pointer', + display: 'inline', + verticalAlign: 'middle', }, - footerIcon : { - display : 'inline', - verticalAlign : 'bottom' + footerIcon: { + display: 'inline', + verticalAlign: 'bottom' }, - icon : { fontSize : 20, }, - notifSuccess : { - backgroundColor : "rgb(248, 252, 248) !important", - color : `${notificationColors.success} !important`, pointerEvents : "auto !important" + icon: { fontSize: 20, }, + notifSuccess: { + backgroundColor: "rgb(248, 252, 248) !important", + color: `${notificationColors.success} !important`, pointerEvents: "auto !important" }, - notifInfo : { - backgroundColor : "rgb(248, 252, 248) !important", - color : `${notificationColors.info} !important`, pointerEvents : "auto !important" + notifInfo: { + backgroundColor: "rgb(248, 252, 248) !important", + color: `${notificationColors.info} !important`, pointerEvents: "auto !important" }, - notifWarn : { - backgroundColor : "rgba(240, 163, 3, 0.04) !important", - color : `${notificationColors.warning} !important`, pointerEvents : "auto !important" + notifWarn: { + backgroundColor: "rgba(240, 163, 3, 0.04) !important", + color: `${notificationColors.warning} !important`, pointerEvents: "auto !important" }, - notifError : { - backgroundColor : "rgb(248, 252, 248) !important", - color : `${notificationColors.error} !important`, - pointerEvents : "auto !important" + notifError: { + backgroundColor: "rgb(248, 252, 248) !important", + color: `${notificationColors.error} !important`, + pointerEvents: "auto !important" }, - darknotifSuccess : { - backgroundColor : "#323232 !important", - color : `${darkNotificationColors.success} !important`, - pointerEvents : "auto !important" + darknotifSuccess: { + backgroundColor: "#323232 !important", + color: `${darkNotificationColors.success} !important`, + pointerEvents: "auto !important" }, - darknotifInfo : { - backgroundColor : "#323232 !important", - color : `${darkNotificationColors.info} !important`, - pointerEvents : "auto !important" + darknotifInfo: { + backgroundColor: "#323232 !important", + color: `${darkNotificationColors.info} !important`, + pointerEvents: "auto !important" }, - darknotifWarn : { - backgroundColor : "#323232 !important", - color : `${darkNotificationColors.warning} !important`, - pointerEvents : "auto !important" + darknotifWarn: { + backgroundColor: "#323232 !important", + color: `${darkNotificationColors.warning} !important`, + pointerEvents: "auto !important" }, - darknotifError : { - backgroundColor : "#323232 !important", - color : `${darkNotificationColors.error} !important`, - pointerEvents : "auto !important" + darknotifError: { + backgroundColor: "#323232 !important", + color: `${darkNotificationColors.error} !important`, + pointerEvents: "auto !important" }, - playgroundFooter : { - backgroundColor : notificationColors.warning, - padding : theme.spacing(2), + playgroundFooter: { + backgroundColor: notificationColors.warning, + padding: theme.spacing(2), } }); \ No newline at end of file From 8fff133dcd55c4cb8ed0f509d103d85b9df181d5 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Tue, 12 Sep 2023 16:36:12 +0530 Subject: [PATCH 04/44] Added Redux and Rtk Signed-off-by: aabidsofi19 --- ui/components/NotificationCenter/filter.js | 36 +++- ui/components/NotificationCenter/index.js | 158 ++++++------------ .../NotificationCenter/notification.js | 31 +++- ui/rtk-query/index.js | 8 + ui/rtk-query/notificationCenter.js | 94 +++++++++++ ui/store/index.js | 3 + ui/store/slices/events.js | 94 ++++++++--- 7 files changed, 285 insertions(+), 139 deletions(-) create mode 100644 ui/rtk-query/index.js create mode 100644 ui/rtk-query/notificationCenter.js diff --git a/ui/components/NotificationCenter/filter.js b/ui/components/NotificationCenter/filter.js index 8e2c908ad4e..6fa7c6b2331 100644 --- a/ui/components/NotificationCenter/filter.js +++ b/ui/components/NotificationCenter/filter.js @@ -16,7 +16,7 @@ import ContentFilterIcon from "../../assets/icons/ContentFilterIcon"; import { useEffect, useReducer, useRef, useState } from "react"; import CrossCircleIcon from "../../assets/icons/CrossCircleIcon"; import clsx from "clsx"; -import { SEVERITY } from "./constants"; +import { SEVERITY, STATUS } from "./constants"; const useStyles = makeStyles((theme) => ({ root: { @@ -78,6 +78,14 @@ const FILTERS = { values: Object.values(SEVERITY), }, + STATUS: { + value: "status", + label: "Status", + description: "Filter by status", + values: Object.values(STATUS), + type: "string" + }, + TYPE: { value: "type", label: "Type", @@ -119,18 +127,32 @@ const Delimiter = { FILTER_VALUE: ":", }; -//return a filter object of form { type : {values} , type2 : {values} } -//from the filter string of form "type:value type2:value2 type:value2" +/** + * Parses a filter string and returns a filter object. + * + * @param {string} filterString - The input filter string of the form "type:value type2:value2 type:value2". + * @returns {Object} - The filter object with types as keys and arrays of values as values. + */ const getFilters = (filterString) => { const filters = {}; const filterValuePairs = filterString.split(Delimiter.FILTER); filterValuePairs.forEach((filterValuePair) => { const [filter, value] = filterValuePair.split(Delimiter.FILTER_VALUE); + + if (filter == FILTERS.STATUS.value) { + filters[filter] = value; + return + } + if (filter && value) { - filters[filter] = filters[filter] || new Set(); - filters[filter].add(value); + filters[filter] = filters[filter] || []; + if (!filters[filter].includes(value)) { + filters[filter].push(value) + } } }); + + return filters; }; @@ -370,9 +392,9 @@ const FilterValueSuggestions = ({ filterStateMachine, dispatchFilterMachine }) = ); }; -const Filter = ({ initialFilter, handleFilter }) => { +const Filter = ({ handleFilter }) => { const theme = useTheme(); - console.log("initialFilter", initialFilter) + // console.log("initialFilter", initialFilter) const classes = useStyles(); const [anchorEl, setAnchorEl] = useState(null); const isPopperOpen = Boolean(anchorEl); diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index 04438b9d3e2..8e8a8b2030a 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -9,12 +9,12 @@ import { loadEventsFromPersistence, toggleNotificationCenter, updateEvents } fro import { iconMedium } from "../../css/icons.styles"; import { bindActionCreators } from "redux"; import { SEVERITY, SEVERITY_STYLE, STATUS, STATUS_STYLE } from "./constants"; -import axios from "axios"; import classNames from "classnames"; import Notification from "./notification"; import { store } from "../../store"; import { useStyles } from "./notificationCenter.style"; -import { clearEvents, setEvents, setEventsSummary } from "../../store/slices/events"; +import { loadEvents, loadNextPage } from "../../store/slices/events"; +import { useGetEventsSummaryQuery, useLazyGetEventsQuery } from "../../rtk-query/notificationCenter"; @@ -35,8 +35,12 @@ const NotificationCountChip = ({ classes, notificationStyle, count }) => { } const Header = () => { - useLoadEventsSummary() - const { count_by_severity_level, total_count } = useSelector((state) => state.events.summary); + + const { data } = useGetEventsSummaryQuery(); + const { count_by_severity_level, total_count } = data || { + count_by_severity_level: [], + total_count: 0 + } const classes = useStyles() const getSeverityCount = (severity) => { return count_by_severity_level.find((item) => item.severity === severity)?.count || 0 @@ -44,7 +48,6 @@ const Header = () => { const archivedCount = count_by_severity_level .reduce((acc, item) => acc + item.count, 0) - total_count - console.log("count_by_severity_level", count_by_severity_level,total_count) return (
@@ -66,91 +69,38 @@ const Header = () => { ) } -const useLoadEvents = (filters, page) => { - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [hasMore, setHasMore] = useState(true) - const events = useSelector((state) => state.events.events); - const dispatch = useDispatch() - useEffect(() => { - setLoading(true) - const parsedFilters = {} - Object.keys(filters).forEach((key) => { - if (filters[key]) { - parsedFilters[key] = JSON.stringify(filters[key], (_key, value) => (value instanceof Set ? [...value] : value)) - } - }) - axios.get(`/api/v2/events`, - { - params: { - ...parsedFilters, - page: page, - page_size: 15 - } - } - ).then(({ data }) => { - if (data.events.length === 0) { - setHasMore(false) - return - } - dispatch(setEvents([...events, ...data.events])) - }).catch((err) => { - setError(err) - }).finally(() => { - setLoading(false) - }) +// } - }, [page, filters]) - - const reset = () => { - dispatch(clearEvents()) - setHasMore(true) - } +const EventsView = () => { + const events = useSelector((state) => state.events.events); + const validEvents = events.filter((event) => event.id) + const page = useSelector((state) => state.events.current_view.page); - return { - loading, - error, - hasMore, - reset, - } + return ( + <> +
Page {page}
+ {validEvents.map((event) => )} + + ) } -const useLoadEventsSummary = () => { - - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) +const MesheryNotification = (props) => { + const [anchorEl, setAnchorEl] = useState(null); const dispatch = useDispatch() - useEffect(() => { - setLoading(true) - axios.get(`/api/v2/events?page=$1&page_size=15`).then(({ data }) => { - dispatch(setEventsSummary({ - count_by_severity_level: data.count_by_severity_level, - total_count: data.total_count - })) - }).catch((err) => { - setError(err) - }).finally(() => { - setLoading(false) - }) - - }, []) - return { - loading, - error, - } -} + const [fetchEvents, { isLoading }] = useLazyGetEventsQuery() + const hasMore = useSelector((state) => state.events.current_view.has_more); -const MesheryNotification = (props) => { - const [anchorEl, setAnchorEl] = useState(null); - const [filters, setFilters] = useState({}) - const [page, setPage] = useState(1) + useEffect(() => { + dispatch(loadEvents(fetchEvents, 1, { + status: STATUS.UNREAD, + })) + }, []) - const events = useSelector((state) => state.events.events); const loadMore = () => { - setPage(page => page + 1) + dispatch(loadNextPage(fetchEvents)) } const handleToggle = () => { @@ -168,36 +118,31 @@ const MesheryNotification = (props) => { const { showFullNotificationCenter } = props; const open = Boolean(anchorEl) || showFullNotificationCenter; - const { loading, hasMore, reset } = useLoadEvents(filters, page) - - const loader = React.useRef(null); - const handleObserver = React.useCallback((entries) => { - const target = entries[0]; - if (target.isIntersecting && !loading && hasMore) { - loadMore(); - } - }, [loading]); - - useEffect(() => { - const option = { - root: null, - rootMargin: "20px", - threshold: 0 - }; - const observer = new IntersectionObserver(handleObserver, option); - if (loader.current) observer.observe(loader.current); - }, [handleObserver]); + // const loader = React.useRef(null); + // const handleObserver = React.useCallback((entries) => { + // const target = entries[0]; + // if (target.isIntersecting && !isLoading && hasMore) { + // loadMore(); + // } + // }, [hasMore, isLoading]); + // + // + // useEffect(() => { + // const option = { + // root: null, + // rootMargin: "20px", + // threshold: 0 + // }; + // const observer = new IntersectionObserver(handleObserver, option); + // if (loader.current) observer.observe(loader.current); + // }, [handleObserver]); const handleFilter = (filters) => { - reset() - setFilters(filters) - setPage(1) + console.log("filtering", filters) + dispatch(loadEvents(fetchEvents, 1, filters)) } - const value = useSelector((state) => state.events.value); - console.log("value", value) - return (
@@ -250,9 +195,8 @@ const MesheryNotification = (props) => {
- {events.map((event) => )} - {loading &&
Loading...
} - {!loading && } + + {!isLoading && hasMore && }
diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js index f131d99dec8..617de049a80 100644 --- a/ui/components/NotificationCenter/notification.js +++ b/ui/components/NotificationCenter/notification.js @@ -1,7 +1,7 @@ import * as React from 'react'; -import { Box, Button, Collapse, Divider, Grid, IconButton, Popover, Typography, alpha, useTheme } from "@material-ui/core" +import { Box, Button, Collapse, Divider, Grid, IconButton, Popover, Typography, alpha, useTheme } from "@material-ui/core" import { makeStyles } from "@material-ui/core" -import { SEVERITY_STYLE } from "./constants" +import { SEVERITY_STYLE, STATUS } from "./constants" import { iconLarge, iconMedium } from "../../css/icons.styles" import { MoreVert } from "@material-ui/icons" import { Avatar } from "@mui/material" @@ -11,7 +11,8 @@ import TwitterIcon from "../../assets/icons/TwitterIcon" import ShareIcon from "../../assets/icons/ShareIcon" import DeleteIcon from "../../assets/icons/DeleteIcon" import moment from 'moment'; - +import { Done } from '@mui/icons-material'; +import { useUpdateStatusMutation } from "../../rtk-query/notificationCenter" const useStyles = makeStyles(() => ({ root: (props) => ({ @@ -164,6 +165,22 @@ function BasicMenu() { ); } +const ChangeStatus = ({ event }) => { + + const newStatus = event.status === STATUS.READ ? STATUS.UNREAD : STATUS.READ + const [updateStatusMutation, { isLoading }] = useUpdateStatusMutation() + + const updateStatus = () => { + updateStatusMutation({ id: event.id, status: newStatus }) + } + return ( + + + + ) + +} + export const Notification = ({ event }) => { const severityStyles = SEVERITY_STYLE[event.severity] const classes = useStyles({ @@ -184,13 +201,16 @@ export const Notification = ({ event }) => { - {event.description} + {event.description} {formatTimestamp(event.update_at)} - + + + + @@ -205,6 +225,7 @@ export const Notification = ({ event }) => { Details + {event.id} {event.description}
diff --git a/ui/rtk-query/index.js b/ui/rtk-query/index.js new file mode 100644 index 00000000000..9be1c74e7bc --- /dev/null +++ b/ui/rtk-query/index.js @@ -0,0 +1,8 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +export const api = createApi({ + reducerPath: 'mesheryApi', + baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), + endpoints: () => ({ + }), +}) \ No newline at end of file diff --git a/ui/rtk-query/notificationCenter.js b/ui/rtk-query/notificationCenter.js new file mode 100644 index 00000000000..76d713b711d --- /dev/null +++ b/ui/rtk-query/notificationCenter.js @@ -0,0 +1,94 @@ +import { api } from "./index" + +/** + * Convert an object with filters into a parsed object. + * + * @param {Object} filters - The input object containing filters. + * @returns {Object} - The parsed object with filters. + */ +function parseFilters(filters) { + return Object.entries(filters).reduce((parsedFilters, [key, value]) => { + if (value || typeof value === 'string') { + parsedFilters[key] = + typeof value === 'string' + ? value + : JSON.stringify(value, (_key, val) => + val instanceof Set ? [...val] : val + ); + } + return parsedFilters; + }, {}); +} +const PROVIDER_TAGS = { + EVENT: "event" +} +export const notificationCenterApi = api + .enhanceEndpoints({ + addTagTypes: Object.values(PROVIDER_TAGS), + }) + .injectEndpoints({ + endpoints: (builder) => ({ + getEvents: builder.query({ + query: ({ + page = 1, + filters = {} + }) => { + + const parsedFilters = parseFilters(filters); + // console.log("parsedFilters", parsedFilters) + return { + url: `v2/events`, + params: { + ...parsedFilters, + page: page, + page_size: 15 + } + } + }, + providesTags: [PROVIDER_TAGS.EVENT], + // keepUnusedDataFor : "0.001" + }), + getEventsSummary: builder.query({ + query: () => { + return { + url: `v2/events?page=$1&page_size=1` + } + }, + transformResponse: (response) => { + return { + count_by_severity_level: response.count_by_severity_level, + total_count: response.total_count + } + }, + providesTags: [PROVIDER_TAGS.EVENT], + }), + + updateStatus: builder.mutation({ + query: ({ id, status }) => ({ + url: `events/status/${id}`, + method: 'POST', + body: { + status: status + } + }), + invalidatesTags: [PROVIDER_TAGS.EVENT], + }), + + deleteEvent: builder.mutation({ + query: ({ id }) => ({ + url: `events/${id}`, + method: 'DELETE', + }), + invalidatesTags: [PROVIDER_TAGS.EVENT], + }) + }), + overrideExisting: false, + }) + +export const { + useGetEventsSummaryQuery, + useUpdateStatusMutation, + useLazyGetEventsQuery, +} = notificationCenterApi + + diff --git a/ui/store/index.js b/ui/store/index.js index b69ea7ff3e9..823010d91f5 100644 --- a/ui/store/index.js +++ b/ui/store/index.js @@ -1,8 +1,11 @@ import eventsReducer from './slices/events' import { configureStore } from '@reduxjs/toolkit' +import { api } from "../rtk-query/index" export const store = configureStore({ reducer: { events: eventsReducer, + [api.reducerPath]: api.reducer, }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), }) \ No newline at end of file diff --git a/ui/store/slices/events.js b/ui/store/slices/events.js index 1c723c7e9e0..4f4b4a3ab9b 100644 --- a/ui/store/slices/events.js +++ b/ui/store/slices/events.js @@ -1,16 +1,27 @@ import { createSlice } from '@reduxjs/toolkit' -import { SEVERITY } from '../../components/NotificationCenter/constants' +import { SEVERITY, STATUS } from '../../components/NotificationCenter/constants' +import _ from 'lodash' const initialState = { events: [], - summary: { - count_by_severity_level: [], - total_count: 0 + // summary: { + // count_by_severity_level: [], + // total_count: 0 + // }, + + current_view: { + page: 1, + page_size: 10, + filters: { + initial: true, + }, + has_more: true, } } const defaultEventProperties = { severity: SEVERITY.INFO, + status: STATUS.UNREAD, } export const eventsSlice = createSlice({ @@ -24,36 +35,79 @@ export const eventsSlice = createSlice({ setEvents: (state, action) => { state.events = action.payload || [] + if (state.events.length == 0) { + state.current_view.has_more = false + } }, - setEventsSummary: (state, action) => { - state.summary = action.payload || [] + + pushEvents: (state, action) => { + state.events = [...state.events, ...action.payload] + if (action.payload.length == 0) { + state.current_view.has_more = false + } }, pushEvent: (state, action) => { const event = { ...action.payload, severity: action.payload?.severity?.trim() || defaultEventProperties.severity, + status: action.payload?.status?.trim() || defaultEventProperties.status, } state.events = [event, ...state.events] - //update severity count - const severity = event.severity - const severityIndex = state.summary.count_by_severity_level.findIndex((item) => item.severity === severity) - if (severityIndex === -1) { - state.summary.count_by_severity_level.push({ - severity: severity, - count: 1 - }) - } else { - state.summary.count_by_severity_level[severityIndex].count += 1 - } - state.summary.total_count += 1 }, + clearCurrentView: (state) => { + state.current_view = initialState.current_view + state.events = [] + }, + + setCurrentView: (state, action) => { + state.current_view = action.payload + } + }, }) // Action creators are generated for each case reducer function -export const { pushEvent, clearEvents, setEvents, setEventsSummary } = eventsSlice.actions +export const { pushEvent, clearEvents, setEvents, + clearCurrentView, + pushEvents, setCurrentView +} = eventsSlice.actions + +export default eventsSlice.reducer + + +export const loadEvents = (fetch, page, filters) => async (dispatch, getState) => { + console.log("loadEvents", page, filters,fetch) + const currentView = getState().events.current_view + if (currentView.page === page && _.isEqual(currentView.filters, filters)) { + console.log("same page and filters") + return + } + + try { + const { data } = await fetch({ page, filters }) + console.log("loadEventsRes", data) + dispatch(setCurrentView({ + ...currentView, + page, + filters + })) + if (page <= 1) { + dispatch(setEvents(data?.events)) + return + } + dispatch(pushEvents(data?.events || [])) + } catch (e) { + console.error("Error while setting events in store --loadEvents", e) + return + } +} + +export const loadNextPage = (fetch) => async (dispatch, getState) => { + const currentView = getState().events.current_view + dispatch(loadEvents(fetch,currentView.page + 1, currentView.filters)) +} + -export default eventsSlice.reducer \ No newline at end of file From 621d14f5a288eaf31fcfaa4c0550e4052cc05c2d Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Tue, 12 Sep 2023 23:26:33 +0530 Subject: [PATCH 05/44] Add Optimistic updates and invalidation Signed-off-by: aabidsofi19 --- ui/components/NotificationCenter/index.js | 65 ++++--- .../NotificationCenter/notification.js | 152 ++++++++++----- ui/pages/_app.js | 184 +++++++++--------- ui/rtk-query/notificationCenter.js | 6 +- ui/store/slices/events.js | 67 +++++-- 5 files changed, 281 insertions(+), 193 deletions(-) diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index 8e8a8b2030a..5661b7aafe0 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import IconButton from "@material-ui/core/IconButton"; import { Provider, connect, useDispatch, useSelector } from "react-redux"; import NoSsr from "@material-ui/core/NoSsr"; @@ -13,8 +13,8 @@ import classNames from "classnames"; import Notification from "./notification"; import { store } from "../../store"; import { useStyles } from "./notificationCenter.style"; -import { loadEvents, loadNextPage } from "../../store/slices/events"; -import { useGetEventsSummaryQuery, useLazyGetEventsQuery } from "../../rtk-query/notificationCenter"; +import { loadEvents, loadNextPage, selectEvents } from "../../store/slices/events"; +import { useGetEventsSummaryQuery, useLazyGetEventsQuery } from "../../rtk-query/notificationCenter"; @@ -70,17 +70,39 @@ const Header = () => { } -// } -const EventsView = () => { - const events = useSelector((state) => state.events.events); - const validEvents = events.filter((event) => event.id) - const page = useSelector((state) => state.events.current_view.page); +const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { + const events = useSelector(selectEvents) + // const page = useSelector((state) => state.events.current_view.page); + + const lastEventRef = useRef(null) + const intersectionObserver = useRef(new IntersectionObserver((entries) => { + const firstEntry = entries[0] + if (firstEntry.isIntersecting) { + handleLoadNextPage() + } + }, { threshold: 1 })) + + useEffect(() => { + const currentObserver = intersectionObserver.current; + if (lastEventRef.current) { + currentObserver.observe(lastEventRef.current); + } + return () => { + if (lastEventRef.current) { + currentObserver.unobserve(lastEventRef.current); + } + }; + }, [lastEventRef.current]); return ( <> -
Page {page}
- {validEvents.map((event) => )} + {events.map((event, idx) =>
+ +
)} + {!isLoading && hasMore && +
} + {isLoading &&
Loading...
} ) } @@ -119,27 +141,7 @@ const MesheryNotification = (props) => { const open = Boolean(anchorEl) || showFullNotificationCenter; - // const loader = React.useRef(null); - // const handleObserver = React.useCallback((entries) => { - // const target = entries[0]; - // if (target.isIntersecting && !isLoading && hasMore) { - // loadMore(); - // } - // }, [hasMore, isLoading]); - // - // - // useEffect(() => { - // const option = { - // root: null, - // rootMargin: "20px", - // threshold: 0 - // }; - // const observer = new IntersectionObserver(handleObserver, option); - // if (loader.current) observer.observe(loader.current); - // }, [handleObserver]); - const handleFilter = (filters) => { - console.log("filtering", filters) dispatch(loadEvents(fetchEvents, 1, filters)) } @@ -195,8 +197,7 @@ const MesheryNotification = (props) => {
- - {!isLoading && hasMore && } +
diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js index 617de049a80..ef344be6213 100644 --- a/ui/components/NotificationCenter/notification.js +++ b/ui/components/NotificationCenter/notification.js @@ -11,14 +11,16 @@ import TwitterIcon from "../../assets/icons/TwitterIcon" import ShareIcon from "../../assets/icons/ShareIcon" import DeleteIcon from "../../assets/icons/DeleteIcon" import moment from 'moment'; -import { Done } from '@mui/icons-material'; -import { useUpdateStatusMutation } from "../../rtk-query/notificationCenter" +import { useUpdateStatusMutation, useDeleteEventMutation } from "../../rtk-query/notificationCenter" +import { useDispatch } from 'react-redux'; +import { changeEventStatus, deleteEvent } from '../../store/slices/events'; const useStyles = makeStyles(() => ({ root: (props) => ({ width: "100%", borderRadius: "3px", border: `1px solid ${props.notificationColor}`, + borderLeftWidth: props.status === STATUS.READ ? "4px" : "1px", marginBlock: "8px", }), @@ -86,6 +88,14 @@ const useMenuStyles = makeStyles((theme) => { // justifyContent: "center", }, + button: { + padding: "0rem", + display: "flex", + gridGap: "0.5rem", + alignItems: "center", + justifyContent: "start", + }, + } }) @@ -100,17 +110,22 @@ const formatTimestamp = (utcTimestamp) => { // return moment(utcTimestamp).fromNow() } -function BasicMenu() { +function BasicMenu({ event }) { const classes = useMenuStyles() const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); + + const handleClick = (event) => { + event.stopPropagation() setAnchorEl(event.currentTarget); }; - const handleClose = () => { + const handleClose = (e) => { + e.stopPropagation() setAnchorEl(null); }; + const theme = useTheme() return (
@@ -151,48 +166,83 @@ function BasicMenu() {
-
- -
+ +
); } -const ChangeStatus = ({ event }) => { +export const DeleteEvent = ({ event }) => { + + const classes = useMenuStyles() + const dispatch = useDispatch() + const [deleteEventMutation] = useDeleteEventMutation() + const theme = useTheme() + const handleDelete = (e) => { + e.stopPropagation() + dispatch(deleteEvent(deleteEventMutation, event.id)) + } + return ( +
+ +
+ ) + +} + + + +export const ChangeStatus = ({ event }) => { + const classes = useMenuStyles() const newStatus = event.status === STATUS.READ ? STATUS.UNREAD : STATUS.READ - const [updateStatusMutation, { isLoading }] = useUpdateStatusMutation() + const [updateStatusMutation] = useUpdateStatusMutation() + + const dispatch = useDispatch() - const updateStatus = () => { - updateStatusMutation({ id: event.id, status: newStatus }) + const updateStatus = (e) => { + e.stopPropagation() + dispatch(changeEventStatus(updateStatusMutation, event.id, newStatus)) } return ( - - - +
+ +
) } +const BulletList = ({ items }) => { + return
    + {[items].map((i) =>
  1. + {i} +
  2. )} +
+} + export const Notification = ({ event }) => { const severityStyles = SEVERITY_STYLE[event.severity] const classes = useStyles({ - notificationColor: severityStyles.color + notificationColor: severityStyles.color, + status: event.status }) - const [expanded, setExpanded] = React.useState(false) const handleExpandClick = (e) => { e.stopPropagation() setExpanded(!expanded); }; + const longDescription = event?.metadata?.error?.LongDescription || [] + const probableCause = event?.metadata?.error?.ProbableCause || [] + const suggestedRemediation = event?.metadata?.error?.SuggestedRemediation || [] + return (
@@ -207,9 +257,8 @@ export const Notification = ({ event }) => { {formatTimestamp(event.update_at)} - - - + + @@ -221,34 +270,17 @@ export const Notification = ({ event }) => {
- - Details - - - {event.id} - {event.description} - + +
+ +
- - - - Probable Cause - -
    - {[1, 2, 3].map((i) =>
  1. - Error Removing Cpx from Kubernetes Context id 23959 . -
  2. )} -
+ + 0 ? 6 : 12}> + - - - Suggested Remediation - -
    - {[1, 2, 3].map((i) =>
  1. - Error Removing Cpx from Kubernetes Context id 23959 . -
  2. )} -
+ 0 ? 6 : 12} > +
@@ -260,4 +292,24 @@ export const Notification = ({ event }) => { } +const NestedData = ({ heading, data, classes }) => { + + if (!data || data?.length == 0) return null + return ( + <> + + {heading} + + {typeof data === "string" ? + + {data} + : + + } + + ) +} + + + export default Notification \ No newline at end of file diff --git a/ui/pages/_app.js b/ui/pages/_app.js index a7d3b1e9bc0..e796b4a05ed 100644 --- a/ui/pages/_app.js +++ b/ui/pages/_app.js @@ -32,7 +32,7 @@ import getPageContext from '../components/PageContext'; import { MESHSYNC_EVENT_SUBSCRIPTION, OPERATOR_EVENT_SUBSCRIPTION } from '../components/subscription/helpers'; import { GQLSubscription } from '../components/subscription/subscriptionhandler'; import dataFetch, { promisifiedDataFetch } from '../lib/data-fetch'; -import { actionTypes, makeStore, toggleCatalogContent,updateTelemetryUrls } from '../lib/store'; +import { actionTypes, makeStore, toggleCatalogContent, updateTelemetryUrls } from '../lib/store'; import theme, { styles } from "../themes"; import { getK8sConfigIdsFromK8sConfig } from '../utils/multi-ctx'; import './../public/static/style/index.css'; @@ -55,7 +55,8 @@ import "./styles/charts.css" import subscribeEvents from '../components/graphql/subscriptions/EventsSubscription'; import { store as rtkStore } from '../store'; import { pushEvent } from '../store/slices/events'; - +import { api as mesheryApi } from "../rtk-query" +import { PROVIDER_TAGS } from '../rtk-query/notificationCenter'; if (typeof window !== 'undefined') { @@ -95,21 +96,21 @@ class MesheryApp extends App { this.fullScreenChanged = this.fullScreenChanged.bind(this); this.state = { - mobileOpen : false, - isDrawerCollapsed : false, - isFullScreenMode : false, - k8sContexts : [], - activeK8sContexts : [], - operatorSubscription : null, - meshSyncSubscription : null, - disposeK8sContextSubscription : null, - theme : 'light', - isOpen : false, - relayEnvironment : createRelayEnvironment(), + mobileOpen: false, + isDrawerCollapsed: false, + isFullScreenMode: false, + k8sContexts: [], + activeK8sContexts: [], + operatorSubscription: null, + meshSyncSubscription: null, + disposeK8sContextSubscription: null, + theme: 'light', + isOpen: false, + relayEnvironment: createRelayEnvironment(), }; } - initMeshSyncEventsSubscription(contexts=[]) { + initMeshSyncEventsSubscription(contexts = []) { if (this.meshsyncEventsSubscriptionRef.current) { this.meshsyncEventsSubscriptionRef.current.dispose(); } @@ -126,7 +127,7 @@ class MesheryApp extends App { const eventType = result.meshsyncevents.type; const spec = result?.meshsyncevents?.object?.spec?.attribute; const status = result?.meshsyncevents?.object?.status?.attribute; - const data = { spec : JSON.parse(spec), status : JSON.parse(status) }; + const data = { spec: JSON.parse(spec), status: JSON.parse(status) }; if (telemetryCompName === TelemetryComps.GRAFANA) { grafanaURLs = grafanaURLs.concat(extractURLFromScanData(data)); @@ -136,11 +137,11 @@ class MesheryApp extends App { updateURLs(promUrlsSet, prometheusURLs, eventType); } - this.props.updateTelemetryUrls({ telemetryURLs : { grafana : Array.from(grafanaUrlsSet), prometheus : Array.from(promUrlsSet) } }) + this.props.updateTelemetryUrls({ telemetryURLs: { grafana: Array.from(grafanaUrlsSet), prometheus: Array.from(promUrlsSet) } }) } }, { - contexts : contexts + contexts: contexts }); this.meshsyncEventsSubscriptionRef.current = meshSyncEventsSubscription; @@ -148,7 +149,7 @@ class MesheryApp extends App { fullScreenChanged = () => { this.setState(state => { - return { isFullScreenMode : !state.isFullScreenMode } + return { isFullScreenMode: !state.isFullScreenMode } }); } @@ -158,13 +159,13 @@ class MesheryApp extends App { dataFetch( "/api/user/prefs", { - method : "GET", - credentials : "include", + method: "GET", + credentials: "include", }, (result) => { if (typeof result?.usersExtensionPreferences?.catalogContent !== 'undefined') { this.props.toggleCatalogContent({ - catalogVisibility : result?.usersExtensionPreferences?.catalogContent + catalogVisibility: result?.usersExtensionPreferences?.catalogContent }) } }, @@ -173,17 +174,17 @@ class MesheryApp extends App { this.initMeshSyncEventsSubscription(this.state.activeK8sContexts); this.initEventsSubscription() - const k8sContextSubscription = (page="", search="", pageSize="10", order="") => { + const k8sContextSubscription = (page = "", search = "", pageSize = "10", order = "") => { return subscribeK8sContext((result) => { - this.setState({ k8sContexts : result.k8sContext }, () => this.setActiveContexts("all")) - this.props.store.dispatch({ type : actionTypes.UPDATE_CLUSTER_CONFIG, k8sConfig : result.k8sContext.contexts }); + this.setState({ k8sContexts: result.k8sContext }, () => this.setActiveContexts("all")) + this.props.store.dispatch({ type: actionTypes.UPDATE_CLUSTER_CONFIG, k8sConfig: result.k8sContext.contexts }); }, { - selector : { - page : page, - pageSize : pageSize, - order : order, - search : search + selector: { + page: page, + pageSize: pageSize, + order: order, + search: search } }) } @@ -204,7 +205,14 @@ class MesheryApp extends App { const eventsSubscription = subscribeEvents(result => { console.log("event received", result); - rtkStore.dispatch(pushEvent(result.event)) + rtkStore.dispatch(pushEvent({ + ...result.event, + user_id: result.event.userID, + updated_at: result.event.updatedAt, + created_at: result.event.createdAt, + deleted_at: result.event.deletedAt, + })) + rtkStore.dispatch(mesheryApi.util.invalidateTags([PROVIDER_TAGS.EVENT])) }) this.eventsSubscriptionRef.current = eventsSubscription; } @@ -238,25 +246,25 @@ class MesheryApp extends App { initSubscriptions = (contexts) => { const operatorCallback = (data) => { - this.props.store.dispatch({ type : actionTypes.SET_OPERATOR_SUBSCRIPTION, operatorState : data }); + this.props.store.dispatch({ type: actionTypes.SET_OPERATOR_SUBSCRIPTION, operatorState: data }); } const meshSyncCallback = (data) => { - this.props.store.dispatch({ type : actionTypes.SET_MESHSYNC_SUBSCRIPTION, meshSyncState : data }); + this.props.store.dispatch({ type: actionTypes.SET_MESHSYNC_SUBSCRIPTION, meshSyncState: data }); } - const operatorSubscription = new GQLSubscription({ type : OPERATOR_EVENT_SUBSCRIPTION, contextIds : contexts, callbackFunction : operatorCallback }) - const meshSyncSubscription = new GQLSubscription({ type : MESHSYNC_EVENT_SUBSCRIPTION, contextIds : contexts, callbackFunction : meshSyncCallback }) + const operatorSubscription = new GQLSubscription({ type: OPERATOR_EVENT_SUBSCRIPTION, contextIds: contexts, callbackFunction: operatorCallback }) + const meshSyncSubscription = new GQLSubscription({ type: MESHSYNC_EVENT_SUBSCRIPTION, contextIds: contexts, callbackFunction: meshSyncCallback }) this.setState({ operatorSubscription, meshSyncSubscription }); } handleDrawerToggle = () => { - this.setState(state => ({ mobileOpen : !state.mobileOpen })); + this.setState(state => ({ mobileOpen: !state.mobileOpen })); } handleL5CommunityClick = () => { - this.setState(state => ({ isOpen : !state.isOpen })); + this.setState(state => ({ isOpen: !state.isOpen })); }; /** @@ -267,7 +275,7 @@ class MesheryApp extends App { if (activeK8sContexts.includes("all")) { activeK8sContexts = ["all"]; } - this.props.store.dispatch({ type : actionTypes.SET_K8S_CONTEXT, selectedK8sContexts : activeK8sContexts }); + this.props.store.dispatch({ type: actionTypes.SET_K8S_CONTEXT, selectedK8sContexts: activeK8sContexts }); } setActiveContexts = (id) => { @@ -278,7 +286,7 @@ class MesheryApp extends App { activeContexts.push(ctx.id) ); activeContexts.push("all"); - this.setState({ activeK8sContexts : activeContexts }, + this.setState({ activeK8sContexts: activeContexts }, () => this.activeContextChangeCallback(this.state.activeK8sContexts)); return; } @@ -288,14 +296,14 @@ class MesheryApp extends App { //pop event if (ids.includes(id)) { ids = ids.filter(id => id !== "all") - return { activeK8sContexts : ids.filter(cid => cid !== id) } + return { activeK8sContexts: ids.filter(cid => cid !== id) } } //push event if (ids.length === this.state.k8sContexts.contexts.length - 1) { ids.push("all"); } - return { activeK8sContexts : [...ids, id] } + return { activeK8sContexts: [...ids, id] } }, () => this.activeContextChangeCallback(this.state.activeK8sContexts)) } } @@ -303,37 +311,37 @@ class MesheryApp extends App { searchContexts = (search = "") => { fetchContexts(10, search) .then(ctx => { - this.setState({ k8sContexts : ctx }) + this.setState({ k8sContexts: ctx }) const active = ctx?.contexts?.find(c => c.is_current_context === true); - if (active) this.setState({ activeK8sContexts : [active?.id] }) + if (active) this.setState({ activeK8sContexts: [active?.id] }) }) .catch(err => console.error(err)) } updateExtensionType = (type) => { - this.props.store.dispatch({ type : actionTypes.UPDATE_EXTENSION_TYPE, extensionType : type }); + this.props.store.dispatch({ type: actionTypes.UPDATE_EXTENSION_TYPE, extensionType: type }); } async loadConfigFromServer() { const { store } = this.props; dataFetch('/api/system/sync', { - method : 'GET', - credentials : 'include', + method: 'GET', + credentials: 'include', }, result => { if (result) { if (result.meshAdapters && result.meshAdapters !== null && result.meshAdapters.length > 0) { - store.dispatch({ type : actionTypes.UPDATE_ADAPTERS_INFO, meshAdapters : result.meshAdapters }); + store.dispatch({ type: actionTypes.UPDATE_ADAPTERS_INFO, meshAdapters: result.meshAdapters }); } if (result.grafana) { const grafanaCfg = Object.assign({ - grafanaURL : "", - grafanaAPIKey : "", - grafanaBoardSearch : "", - grafanaBoards : [], - selectedBoardsConfigs : [] + grafanaURL: "", + grafanaAPIKey: "", + grafanaBoardSearch: "", + grafanaBoards: [], + selectedBoardsConfigs: [] }, result.grafana) - store.dispatch({ type : actionTypes.UPDATE_GRAFANA_CONFIG, grafana : grafanaCfg }); + store.dispatch({ type: actionTypes.UPDATE_GRAFANA_CONFIG, grafana: grafanaCfg }); } if (result.prometheus) { if (typeof result.prometheus.prometheusURL === 'undefined') { @@ -343,25 +351,25 @@ class MesheryApp extends App { result.prometheus.selectedPrometheusBoardsConfigs = []; } const promCfg = Object.assign({ - prometheusURL : "", - selectedPrometheusBoardsConfigs : [] + prometheusURL: "", + selectedPrometheusBoardsConfigs: [] }, result.prometheus) - store.dispatch({ type : actionTypes.UPDATE_PROMETHEUS_CONFIG, prometheus : promCfg }); + store.dispatch({ type: actionTypes.UPDATE_PROMETHEUS_CONFIG, prometheus: promCfg }); } if (result.loadTestPrefs) { const loadTestPref = Object.assign({ - c : 0, - qps : 0, - t : 0, - gen : 0 + c: 0, + qps: 0, + t: 0, + gen: 0 }, result.loadTestPrefs) - store.dispatch({ type : actionTypes.UPDATE_LOAD_GEN_CONFIG, loadTestPref }); + store.dispatch({ type: actionTypes.UPDATE_LOAD_GEN_CONFIG, loadTestPref }); } if (typeof result.anonymousUsageStats !== 'undefined') { - store.dispatch({ type : actionTypes.UPDATE_ANONYMOUS_USAGE_STATS, anonymousUsageStats : result.anonymousUsageStats }); + store.dispatch({ type: actionTypes.UPDATE_ANONYMOUS_USAGE_STATS, anonymousUsageStats: result.anonymousUsageStats }); } if (typeof result.anonymousPerfResults !== 'undefined') { - store.dispatch({ type : actionTypes.UPDATE_ANONYMOUS_PERFORMANCE_RESULTS, anonymousPerfResults : result.anonymousPerfResults }); + store.dispatch({ type: actionTypes.UPDATE_ANONYMOUS_PERFORMANCE_RESULTS, anonymousPerfResults: result.anonymousPerfResults }); } } }, error => { @@ -376,7 +384,7 @@ class MesheryApp extends App { return { pageProps }; } themeSetter = (thememode) => { - this.setState({ theme : thememode }) + this.setState({ theme: thememode }) }; render() { const { @@ -414,25 +422,25 @@ class MesheryApp extends App {
, - error : , - warning : , - info : + success: , + error: , + warning: , + info: }} classes={{ - variantSuccess : this.state.theme === "dark" ? classes.darknotifSuccess : classes.notifSuccess, - variantError : this.state.theme === "dark" ? classes.darknotifError : classes.notifError, - variantWarning : this.state.theme === "dark" ? classes.darknotifWarn : classes.notifWarn, - variantInfo : this.state.theme === "dark" ? classes.darknotifInfo : classes.notifInfo, + variantSuccess: this.state.theme === "dark" ? classes.darknotifSuccess : classes.notifSuccess, + variantError: this.state.theme === "dark" ? classes.darknotifError : classes.notifError, + variantWarning: this.state.theme === "dark" ? classes.darknotifWarn : classes.notifWarn, + variantInfo: this.state.theme === "dark" ? classes.darknotifInfo : classes.notifInfo, }} maxSnack={10} > - {!this.state.isFullScreenMode &&
-
+
{this.props.capabilitiesRegistry?.restrictedAccess?.isMesheryUiRestricted ? "ACCESS LIMITED IN MESHERY PLAYGROUND. DEPLOY MESHERY TO ACCESS ALL FEATURES." : (<> Built with by the Layer5 Community)} @@ -470,7 +478,7 @@ class MesheryApp extends App {
- this.setState({ isOpen : false })} isOpen={this.state.isOpen} /> + this.setState({ isOpen: false })} isOpen={this.state.isOpen} /> @@ -479,20 +487,20 @@ class MesheryApp extends App { } } -MesheryApp.propTypes = { classes : PropTypes.object.isRequired, }; +MesheryApp.propTypes = { classes: PropTypes.object.isRequired, }; const mapStateToProps = state => ({ - isDrawerCollapsed : state.get("isDrawerCollapsed"), - k8sConfig : state.get("k8sConfig"), - operatorSubscription : state.get("operatorSubscription"), - meshSyncSubscription : state.get("meshSyncSubscription"), - capabilitiesRegistry : state.get("capabilitiesRegistry"), - telemetryURLs : state.get("telemetryURLs"), + isDrawerCollapsed: state.get("isDrawerCollapsed"), + k8sConfig: state.get("k8sConfig"), + operatorSubscription: state.get("operatorSubscription"), + meshSyncSubscription: state.get("meshSyncSubscription"), + capabilitiesRegistry: state.get("capabilitiesRegistry"), + telemetryURLs: state.get("telemetryURLs"), }) const mapDispatchToProps = dispatch => ({ - toggleCatalogContent : bindActionCreators(toggleCatalogContent, dispatch), - updateTelemetryUrls : bindActionCreators(updateTelemetryUrls, dispatch) + toggleCatalogContent: bindActionCreators(toggleCatalogContent, dispatch), + updateTelemetryUrls: bindActionCreators(updateTelemetryUrls, dispatch) }) const MesheryWithRedux = withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(MesheryApp)); @@ -516,6 +524,6 @@ const MesheryAppWrapper = (props) => { // deserializeState : state => fromJS(state) // })(MesheryAppWrapper)); export default withStyles(styles)(withRedux(makeStore, { - serializeState : state => state.toJS(), - deserializeState : state => fromJS(state) + serializeState: state => state.toJS(), + deserializeState: state => fromJS(state) })(MesheryAppWrapper)); \ No newline at end of file diff --git a/ui/rtk-query/notificationCenter.js b/ui/rtk-query/notificationCenter.js index 76d713b711d..16241008097 100644 --- a/ui/rtk-query/notificationCenter.js +++ b/ui/rtk-query/notificationCenter.js @@ -19,7 +19,7 @@ function parseFilters(filters) { return parsedFilters; }, {}); } -const PROVIDER_TAGS = { +export const PROVIDER_TAGS = { EVENT: "event" } export const notificationCenterApi = api @@ -41,6 +41,8 @@ export const notificationCenterApi = api params: { ...parsedFilters, page: page, + sort: "updated_at", + order: "desc", page_size: 15 } } @@ -88,7 +90,7 @@ export const notificationCenterApi = api export const { useGetEventsSummaryQuery, useUpdateStatusMutation, + useDeleteEventMutation, useLazyGetEventsQuery, } = notificationCenterApi - diff --git a/ui/store/slices/events.js b/ui/store/slices/events.js index 4f4b4a3ab9b..d6810105094 100644 --- a/ui/store/slices/events.js +++ b/ui/store/slices/events.js @@ -1,14 +1,7 @@ -import { createSlice } from '@reduxjs/toolkit' +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' import { SEVERITY, STATUS } from '../../components/NotificationCenter/constants' -import _ from 'lodash' const initialState = { - events: [], - // summary: { - // count_by_severity_level: [], - // total_count: 0 - // }, - current_view: { page: 1, page_size: 10, @@ -24,9 +17,20 @@ const defaultEventProperties = { status: STATUS.UNREAD, } +const eventsEntityAdapter = createEntityAdapter({ + selectId: (event) => event.id, + //sort based on update_at timestamp(utc) + sortComparer: (a, b) => { + if (b?.updated_at?.localeCompare && a?.updated_at?.localeCompare) { + return b.updated_at?.localeCompare(a.updated_at) + } + return 0 + } +}) + export const eventsSlice = createSlice({ name: 'events', - initialState, + initialState: eventsEntityAdapter.getInitialState(initialState), reducers: { clearEvents: (state) => { @@ -34,15 +38,18 @@ export const eventsSlice = createSlice({ }, setEvents: (state, action) => { - state.events = action.payload || [] - if (state.events.length == 0) { + // state.events = action.payload || [] + eventsEntityAdapter.removeAll(state) + eventsEntityAdapter.addMany(state, action.payload) + if (action.payload.length == 0) { state.current_view.has_more = false } }, pushEvents: (state, action) => { - state.events = [...state.events, ...action.payload] + // state.events = [...state.events, ...action.payload] + eventsEntityAdapter.addMany(state, action.payload) if (action.payload.length == 0) { state.current_view.has_more = false } @@ -54,9 +61,13 @@ export const eventsSlice = createSlice({ severity: action.payload?.severity?.trim() || defaultEventProperties.severity, status: action.payload?.status?.trim() || defaultEventProperties.status, } - state.events = [event, ...state.events] + eventsEntityAdapter.addOne(state, event) + // state.events = [event, ...state.events] }, + updateEvent: eventsEntityAdapter.updateOne, + deleteEvent: eventsEntityAdapter.removeOne, + clearCurrentView: (state) => { state.current_view = initialState.current_view state.events = [] @@ -72,23 +83,17 @@ export const eventsSlice = createSlice({ // Action creators are generated for each case reducer function export const { pushEvent, clearEvents, setEvents, clearCurrentView, - pushEvents, setCurrentView + pushEvents, setCurrentView, updateEvent, deleteEvent: removeEvent } = eventsSlice.actions export default eventsSlice.reducer export const loadEvents = (fetch, page, filters) => async (dispatch, getState) => { - console.log("loadEvents", page, filters,fetch) const currentView = getState().events.current_view - if (currentView.page === page && _.isEqual(currentView.filters, filters)) { - console.log("same page and filters") - return - } try { const { data } = await fetch({ page, filters }) - console.log("loadEventsRes", data) dispatch(setCurrentView({ ...currentView, page, @@ -107,7 +112,27 @@ export const loadEvents = (fetch, page, filters) => async (dispatch, getState) = export const loadNextPage = (fetch) => async (dispatch, getState) => { const currentView = getState().events.current_view - dispatch(loadEvents(fetch,currentView.page + 1, currentView.filters)) + dispatch(loadEvents(fetch, currentView.page + 1, currentView.filters)) +} + +export const changeEventStatus = (mutator, id, status) => async (dispatch) => { + dispatch(updateEvent({ id, changes: { status } })) + mutator({ id, status }) +} + +export const deleteEvent = (mutator, id) => async (dispatch) => { + dispatch(removeEvent(id)) + mutator({ id }) } +//selectors + +//select all events +export const selectEvents = (state) => { + return eventsEntityAdapter.getSelectors().selectAll(state.events) +} + +export const selectEventById = (state, id) => { + return eventsEntityAdapter.getSelectors().selectById(state.events, id) +} \ No newline at end of file From 9f456cb7d0f47323146546d4bc4bee66f3e61fc1 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Wed, 13 Sep 2023 14:56:01 +0530 Subject: [PATCH 06/44] Added toaster , global filtering and mobile view fixes Signed-off-by: aabidsofi19 --- ui/components/NotificationCenter/constants.js | 2 +- ui/components/NotificationCenter/filter.js | 5 + ui/components/NotificationCenter/index.js | 150 +++++++++++++----- .../NotificationCenter/notification.js | 19 ++- .../notificationCenter.style.js | 11 +- ui/pages/_app.js | 86 +++++++--- ui/rtk-query/notificationCenter.js | 3 +- ui/store/slices/events.js | 30 ++-- ui/utils/hooks/useNotification.js | 56 ++++--- 9 files changed, 242 insertions(+), 120 deletions(-) diff --git a/ui/components/NotificationCenter/constants.js b/ui/components/NotificationCenter/constants.js index 32af86a4da7..0510e90e2e6 100644 --- a/ui/components/NotificationCenter/constants.js +++ b/ui/components/NotificationCenter/constants.js @@ -17,7 +17,7 @@ export const STATUS = { } export const STATUS_STYLE = { - [STATUS.UNREAD]: { + [STATUS.READ]: { icon: ArchiveIcon, color: Colors.charcoal } diff --git a/ui/components/NotificationCenter/filter.js b/ui/components/NotificationCenter/filter.js index 6fa7c6b2331..e17a2a36cd3 100644 --- a/ui/components/NotificationCenter/filter.js +++ b/ui/components/NotificationCenter/filter.js @@ -408,6 +408,11 @@ const Filter = ({ handleFilter }) => { }); const handleFilterChange = (e) => { + + if (!anchorEl) { + setAnchorEl(e.currentTarget) + } + if (e.target.value === "") { return dispatch({ type: FILTER_EVENTS.CLEAR, diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index 5661b7aafe0..a8f2c8622ff 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -1,25 +1,23 @@ import React, { useEffect, useRef, useState } from "react"; import IconButton from "@material-ui/core/IconButton"; -import { Provider, connect, useDispatch, useSelector } from "react-redux"; +import { Provider, useDispatch, useSelector } from "react-redux"; import NoSsr from "@material-ui/core/NoSsr"; -import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha } from "@material-ui/core"; +import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha, Chip, Button } from "@material-ui/core"; import Filter from "./filter"; import BellIcon from "../../assets/icons/BellIcon.js" -import { loadEventsFromPersistence, toggleNotificationCenter, updateEvents } from "../../lib/store"; import { iconMedium } from "../../css/icons.styles"; -import { bindActionCreators } from "redux"; import { SEVERITY, SEVERITY_STYLE, STATUS, STATUS_STYLE } from "./constants"; import classNames from "classnames"; import Notification from "./notification"; import { store } from "../../store"; import { useStyles } from "./notificationCenter.style"; -import { loadEvents, loadNextPage, selectEvents } from "../../store/slices/events"; +import { closeNotificationCenter, loadEvents, loadNextPage, selectEvents, toggleNotificationCenter } from "../../store/slices/events"; import { useGetEventsSummaryQuery, useLazyGetEventsQuery } from "../../rtk-query/notificationCenter"; +import _ from "lodash"; - -const NotificationCountChip = ({ classes, notificationStyle, count }) => { +const NotificationCountChip = ({ classes, notificationStyle, count, handleClick }) => { const chipStyles = { fill: notificationStyle.color, height: "20px", @@ -27,14 +25,18 @@ const NotificationCountChip = ({ classes, notificationStyle, count }) => { } count = Number(count).toLocaleString('en', { useGrouping: true }) return ( -
- {} - {count} -
+ ) } -const Header = () => { +const Header = ({ handleFilter }) => { const { data } = useGetEventsSummaryQuery(); const { count_by_severity_level, total_count } = data || { @@ -46,6 +48,18 @@ const Header = () => { return count_by_severity_level.find((item) => item.severity === severity)?.count || 0 } + const onClickSeverity = (severity) => { + handleFilter({ + severity: [severity] + }) + } + + const onClickStatus = (status) => { + handleFilter({ + status: status + }) + } + const archivedCount = count_by_severity_level .reduce((acc, item) => acc + item.count, 0) - total_count return ( @@ -58,11 +72,14 @@ const Header = () => {
{Object.values(SEVERITY).map(severity => ( - onClickSeverity(severity)} notificationStyle={SEVERITY_STYLE[severity]} count={getSeverityCount(severity)} />) )} - + onClickStatus(STATUS.READ)} + count={archivedCount} />
@@ -80,6 +97,8 @@ const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { const firstEntry = entries[0] if (firstEntry.isIntersecting) { handleLoadNextPage() + + console.log("intersecting") } }, { threshold: 1 })) @@ -97,7 +116,7 @@ const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { return ( <> - {events.map((event, idx) =>
+ {events.map((event, idx) =>
)} {!isLoading && hasMore && @@ -107,11 +126,54 @@ const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { ) } +const CurrentFilterView = ({ handleFilter }) => { + + const currentFilters = useSelector((state) => state.events.current_view.filters); + + const onDelete = (key, value) => { + const newFilters = { + ...currentFilters, + [key]: typeof currentFilters[key] === "string" ? null : currentFilters[key].filter((item) => item !== value) + } + handleFilter(newFilters) + } + + const Chips = ({ type, value }) => { + if (typeof value === "string") { + return onDelete(type, value)} /> + } + + if (_.isArray(value) && value.length > 0) { + return ( +
+ {value.map((item) => onDelete(type, item)} />)} +
+ ) + } -const MesheryNotification = (props) => { + return null + } + + return ( +
+ {Object.entries(currentFilters).map(([key, value]) => { + if (value && value?.length > 0) { + return
+ {key}: + +
+ } + })} + +
+ ) +} + + +const MesheryNotification = () => { const [anchorEl, setAnchorEl] = useState(null); const dispatch = useDispatch() - + const isNotificationCenterOpen = useSelector((state) => state.events.isNotificationCenterOpen); const [fetchEvents, { isLoading }] = useLazyGetEventsQuery() const hasMore = useSelector((state) => state.events.current_view.has_more); @@ -126,19 +188,19 @@ const MesheryNotification = (props) => { } const handleToggle = () => { - props.toggleOpen(); + dispatch(toggleNotificationCenter()) }; const handleClose = () => { - if (!props.showFullNotificationCenter) { - return; + if (!isNotificationCenterOpen) { + return } - props.toggleOpen(); + dispatch(closeNotificationCenter()) setAnchorEl(null); }; const classes = useStyles() - const { showFullNotificationCenter } = props; - const open = Boolean(anchorEl) || showFullNotificationCenter; + // const { showFullNotificationCenter } = props; + const open = Boolean(anchorEl) || isNotificationCenterOpen; const handleFilter = (filters) => { @@ -187,16 +249,17 @@ const MesheryNotification = (props) => { open={open} classes={{ paper: classes.notificationDrawer, - paperAnchorRight: showFullNotificationCenter ? classes.fullView : classes.peekView, + paperAnchorRight: isNotificationCenterOpen ? classes.fullView : classes.peekView, }} >
-
+
+
@@ -208,23 +271,22 @@ const MesheryNotification = (props) => { ); }; -const mapDispatchToProps = (dispatch) => ({ - updateEvents: bindActionCreators(updateEvents, dispatch), - toggleOpen: bindActionCreators(toggleNotificationCenter, dispatch), - loadEventsFromPersistence: bindActionCreators(loadEventsFromPersistence, dispatch), -}); - -const mapStateToProps = (state) => { - const events = state.get("events"); - return { - user: state.get("user"), - events: events.toJS(), - openEventId: state.get("notificationCenter").get("openEventId"), - showFullNotificationCenter: state.get("notificationCenter").get("showFullNotificationCenter"), - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)((props) => { +// const mapDispatchToProps = (dispatch) => ({ +// updateEvents: bindActionCreators(updateEvents, dispatch), +// toggleOpen: bindActionCreators(toggleNotificationCenter, dispatch), +// loadEventsFromPersistence: bindActionCreators(loadEventsFromPersistence, dispatch), +// }); +// +// const mapStateToProps = (state) => { +// const events = state.get("events"); +// return { +// user: state.get("user"), +// events: events.toJS(), +// openEventId: state.get("notificationCenter").get("openEventId"), +// showFullNotificationCenter: state.get("notificationCenter").get("showFullNotificationCenter"), +// }; +// }; +const NotificationCenter = (props) => { return ( <> @@ -234,4 +296,6 @@ export default connect(mapStateToProps, mapDispatchToProps)((props) => { ) -}); \ No newline at end of file +}; + +export default NotificationCenter; \ No newline at end of file diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js index ef344be6213..9738302c1d7 100644 --- a/ui/components/NotificationCenter/notification.js +++ b/ui/components/NotificationCenter/notification.js @@ -40,6 +40,9 @@ const useStyles = makeStyles(() => ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", + overflowWrap: "break-word", + // max of min of 20rem or 50vw + maxWidth: "min(25rem, 50vw)", width: "100%", }, expanded: { @@ -101,13 +104,13 @@ const useMenuStyles = makeStyles((theme) => { const formatTimestamp = (utcTimestamp) => { - // const curretUtcTimestamp = moment.utc().valueOf() + const currentUtcTimestamp = moment.utc().valueOf() - // const timediff = currentUtcTimestamp - utcTimestamp - // if (timediff >= 24 * 60 * 60 * 1000) { - return moment(utcTimestamp).local().format('MMM DD, YYYY') - // } - // return moment(utcTimestamp).fromNow() + const timediff = currentUtcTimestamp - utcTimestamp + if (timediff >= 24 * 60 * 60 * 1000) { + return moment(utcTimestamp).local().format('MMM DD, YYYY') + } + return moment(utcTimestamp).fromNow() } function BasicMenu({ event }) { @@ -212,7 +215,7 @@ export const ChangeStatus = ({ event }) => { return (
) @@ -254,7 +257,7 @@ export const Notification = ({ event }) => { {event.description} - {formatTimestamp(event.update_at)} + {formatTimestamp(event.created_at)} diff --git a/ui/components/NotificationCenter/notificationCenter.style.js b/ui/components/NotificationCenter/notificationCenter.style.js index 6003e25f0ba..a289ef1c1ed 100644 --- a/ui/components/NotificationCenter/notificationCenter.style.js +++ b/ui/components/NotificationCenter/notificationCenter.style.js @@ -1,7 +1,10 @@ import { makeStyles } from "@material-ui/core" export const useStyles = makeStyles((theme) => ({ - sidelist: { width: "45rem" }, + sidelist: { + width: "45rem", + maxWidth: "90vw", + }, notificationButton: { height: "100%" }, notificationDrawer: { backgroundColor: theme.palette.secondary.drawer, @@ -57,9 +60,10 @@ export const useStyles = makeStyles((theme) => ({ borderRadius: "4px", display: "flex", gap: "4px", - // alignItems: "center", - padding: "4px 12px", + justifyContent: "start", + alignItems: "center", fontSize: "16px", + cursor: "pointer", }, severityChips: { @@ -73,4 +77,3 @@ export const useStyles = makeStyles((theme) => ({ }, })); - diff --git a/ui/pages/_app.js b/ui/pages/_app.js index e796b4a05ed..7229c56c191 100644 --- a/ui/pages/_app.js +++ b/ui/pages/_app.js @@ -23,7 +23,7 @@ import App from 'next/app'; import Head from 'next/head'; import { SnackbarProvider } from 'notistack'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { connect, Provider } from "react-redux"; import Header from '../components/Header'; import MesheryProgressBar from '../components/MesheryProgressBar'; @@ -57,6 +57,8 @@ import { store as rtkStore } from '../store'; import { pushEvent } from '../store/slices/events'; import { api as mesheryApi } from "../rtk-query" import { PROVIDER_TAGS } from '../rtk-query/notificationCenter'; +import { useNotification } from '../utils/hooks/useNotification'; + if (typeof window !== 'undefined') { @@ -74,6 +76,39 @@ if (typeof window !== 'undefined') { } } +const EventsSubsciptionProvider = () => { + + const { notify } = useNotification(); + + const eventsSubscription = useCallback(() => subscribeEvents(result => { + console.log("event received", result); + rtkStore.dispatch(pushEvent({ + ...result.event, + user_id: result.event.userID, + updated_at: result.event.updatedAt, + created_at: result.event.createdAt, + deleted_at: result.event.deletedAt, + })) + rtkStore.dispatch(mesheryApi.util.invalidateTags([PROVIDER_TAGS.EVENT])) + notify({ + message: result.event.description, + event_type: result.event.severity, + id: result.event.id, + showInNotificationCenter: true, + }) + }), []) + + useEffect(() => { + const subscription = eventsSubscription(); + return () => { + subscription.dispose(); + } + }, []) + + return null; + +} + async function fetchContexts(number = 10, search = "") { return await promisifiedDataFetch(`/api/system/kubernetes/contexts?pagesize=${number}&search=${encodeURIComponent(search)}`) } @@ -94,7 +129,6 @@ class MesheryApp extends App { this.meshsyncEventsSubscriptionRef = React.createRef(); this.eventsSubscriptionRef = React.createRef(); this.fullScreenChanged = this.fullScreenChanged.bind(this); - this.state = { mobileOpen: false, isDrawerCollapsed: false, @@ -106,10 +140,11 @@ class MesheryApp extends App { disposeK8sContextSubscription: null, theme: 'light', isOpen: false, - relayEnvironment: createRelayEnvironment(), + relayEnvironment: createRelayEnvironment() }; } + initMeshSyncEventsSubscription(contexts = []) { if (this.meshsyncEventsSubscriptionRef.current) { this.meshsyncEventsSubscriptionRef.current.dispose(); @@ -173,7 +208,7 @@ class MesheryApp extends App { ) this.initMeshSyncEventsSubscription(this.state.activeK8sContexts); - this.initEventsSubscription() + // this.initEventsSubscription() const k8sContextSubscription = (page = "", search = "", pageSize = "10", order = "") => { return subscribeK8sContext((result) => { this.setState({ k8sContexts: result.k8sContext }, () => this.setActiveContexts("all")) @@ -198,24 +233,29 @@ class MesheryApp extends App { document.removeEventListener("fullscreenchange", this.fullScreenChanged); } - initEventsSubscription() { - if (this.eventsSubscriptionRef.current) { - this.eventsSubscriptionRef.current.dispose(); - } - - const eventsSubscription = subscribeEvents(result => { - console.log("event received", result); - rtkStore.dispatch(pushEvent({ - ...result.event, - user_id: result.event.userID, - updated_at: result.event.updatedAt, - created_at: result.event.createdAt, - deleted_at: result.event.deletedAt, - })) - rtkStore.dispatch(mesheryApi.util.invalidateTags([PROVIDER_TAGS.EVENT])) - }) - this.eventsSubscriptionRef.current = eventsSubscription; - } + // initEventsSubscription() { + // if (this.eventsSubscriptionRef.current) { + // this.eventsSubscriptionRef.current.dispose(); + // } + // const notify = this.props.notify; + // const eventsSubscription = subscribeEvents(result => { + // console.log("event received", result); + // rtkStore.dispatch(pushEvent({ + // ...result.event, + // user_id: result.event.userID, + // updated_at: result.event.updatedAt, + // created_at: result.event.createdAt, + // deleted_at: result.event.deletedAt, + // })) + // rtkStore.dispatch(mesheryApi.util.invalidateTags([PROVIDER_TAGS.EVENT])) + // notify({ + // message: result.event.description, + // severity: result.event.severity, + // id: result.event.id, + // }) + // }) + // this.eventsSubscriptionRef.current = eventsSubscription; + // } componentDidUpdate(prevProps) { const { k8sConfig, capabilitiesRegistry } = this.props; @@ -439,6 +479,8 @@ class MesheryApp extends App { }} maxSnack={10} > + + {!this.state.isFullScreenMode &&
event.id, //sort based on update_at timestamp(utc) sortComparer: (a, b) => { - if (b?.updated_at?.localeCompare && a?.updated_at?.localeCompare) { - return b.updated_at?.localeCompare(a.updated_at) + if (b?.created_at?.localeCompare && a?.created_at?.localeCompare) { + return b.created_at?.localeCompare(a.created_at) } return 0 } @@ -41,18 +43,15 @@ export const eventsSlice = createSlice({ // state.events = action.payload || [] eventsEntityAdapter.removeAll(state) eventsEntityAdapter.addMany(state, action.payload) - if (action.payload.length == 0) { - state.current_view.has_more = false - } + + state.current_view.has_more = action.payload.length == 0 ? false : true }, pushEvents: (state, action) => { // state.events = [...state.events, ...action.payload] eventsEntityAdapter.addMany(state, action.payload) - if (action.payload.length == 0) { - state.current_view.has_more = false - } + state.current_view.has_more = action.payload.length == 0 ? false : true }, pushEvent: (state, action) => { @@ -75,7 +74,15 @@ export const eventsSlice = createSlice({ setCurrentView: (state, action) => { state.current_view = action.payload - } + }, + + toggleNotificationCenter: (state) => { + state.isNotificationCenterOpen = !state.isNotificationCenterOpen + }, + + closeNotificationCenter: (state) => { + state.isNotificationCenterOpen = false + }, }, }) @@ -83,7 +90,8 @@ export const eventsSlice = createSlice({ // Action creators are generated for each case reducer function export const { pushEvent, clearEvents, setEvents, clearCurrentView, - pushEvents, setCurrentView, updateEvent, deleteEvent: removeEvent + pushEvents, setCurrentView, updateEvent, deleteEvent: removeEvent, + toggleNotificationCenter, closeNotificationCenter } = eventsSlice.actions export default eventsSlice.reducer diff --git a/ui/utils/hooks/useNotification.js b/ui/utils/hooks/useNotification.js index 8deb4aef76f..39a49c91a37 100644 --- a/ui/utils/hooks/useNotification.js +++ b/ui/utils/hooks/useNotification.js @@ -1,15 +1,17 @@ +/* eslint-disable no-unused-vars */ +//NOTE: This file is being refactored to use the new notification center import { IconButton } from "@material-ui/core" import { ToggleButtonGroup } from "@mui/material" import { useSnackbar } from "notistack" import { iconMedium } from "../../css/icons.styles" import CloseIcon from "@material-ui/icons/Close"; import { useDispatch } from "react-redux"; -import { pushEvent ,openEventInNotificationCenter, toggleNotificationCenter } from "../../lib/store"; import moment from "moment"; import { v4 } from "uuid"; import BellIcon from '@material-ui/icons/Notifications'; import { NOTIFICATION_STATUS } from "../../lib/event-types"; - +import { store as rtkStore } from "../../store/index" +import { toggleNotificationCenter } from "../../store/slices/events"; /** * A React hook to facilitate emitting events from the client. @@ -19,10 +21,10 @@ import { NOTIFICATION_STATUS } from "../../lib/event-types"; * @returns {Object} An object with the `notify` property. */ export const useNotification = () => { + const x = useSnackbar() + console.log("useSnackbar", x) + const { enqueueSnackbar, closeSnackbar } = useSnackbar() - const { enqueueSnackbar,closeSnackbar }= useSnackbar() - - const dispatch = useDispatch() /** * Opens an event in the notification center. @@ -30,12 +32,10 @@ export const useNotification = () => { * @param {string} eventId - The ID of the event to be opened. */ const openEvent = (eventId) => { - dispatch(toggleNotificationCenter()) - dispatch(openEventInNotificationCenter({ - eventId - })) + rtkStore.dispatch(toggleNotificationCenter()) } + /** * Notifies and stores the event. * @@ -47,34 +47,32 @@ export const useNotification = () => { * @param {number} options.timestamp - UTC timestamp for the event. If not provided, it is generated on the client. * @param {Object} options.customEvent - Additional properties related to the event. * @param {boolean} options.showInNotificationCenter - Whether to show the event in the notification center. Defaults to `true`. + * @param {boolean} options.pushToServer - Whether to push the event to the server. Defaults to `false`. */ - const notify = ({ id=null,message,details=null,event_type,timestamp=null,customEvent=null,showInNotificationCenter=true }) => { + const notify = ({ + id = null, message, + details = null, event_type, + timestamp = null, customEvent = null, + showInNotificationCenter = false, + pushToServer = false }) => { timestamp = timestamp ?? moment.utc().valueOf() - id = id || v4() + id = id || v4() + - if (showInNotificationCenter) { - dispatch(pushEvent({ event : { - ...customEvent, - summary : message , - id, - status : NOTIFICATION_STATUS.NEW, - event_type, - timestamp, - details, - } })) - } enqueueSnackbar( - message,{ - variant : event_type.type, - action : function Action(key) { + message, + { + //NOTE: Need to Consolidate the variant and event_type + variant: typeof event_type === "string" ? event_type : event_type?.type, + action: function Action(key) { return ( {showInNotificationCenter && - openEvent(id)}> - - } + openEvent(id)}> + + } closeSnackbar(key)}> @@ -95,7 +93,7 @@ export const useNotification = () => { * @param {React.Component} Component - The class-based component to be wrapped. * @returns {React.Component} The wrapped component with the `notify` prop. */ -export function withNotify( Component ){ +export function withNotify(Component) { return function WrappedWithNotify(props) { const { notify } = useNotification() return From 65345513165e00412f5beff0aa7a0d6102408b67 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Wed, 13 Sep 2023 16:10:03 +0530 Subject: [PATCH 07/44] Add badge counts Signed-off-by: aabidsofi19 --- ui/components/NotificationCenter/index.js | 48 ++++++++++++++----- .../notificationCenter.style.js | 8 ++++ ui/themes/app.js | 2 +- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index a8f2c8622ff..bd6a3ff4be0 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import IconButton from "@material-ui/core/IconButton"; import { Provider, useDispatch, useSelector } from "react-redux"; import NoSsr from "@material-ui/core/NoSsr"; -import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha, Chip, Button } from "@material-ui/core"; +import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha, Chip, Button, Badge } from "@material-ui/core"; import Filter from "./filter"; import BellIcon from "../../assets/icons/BellIcon.js" import { iconMedium } from "../../css/icons.styles"; @@ -10,12 +10,42 @@ import { SEVERITY, SEVERITY_STYLE, STATUS, STATUS_STYLE } from "./constants"; import classNames from "classnames"; import Notification from "./notification"; import { store } from "../../store"; -import { useStyles } from "./notificationCenter.style"; +import { useNavNotificationIconStyles, useStyles } from "./notificationCenter.style"; import { closeNotificationCenter, loadEvents, loadNextPage, selectEvents, toggleNotificationCenter } from "../../store/slices/events"; import { useGetEventsSummaryQuery, useLazyGetEventsQuery } from "../../rtk-query/notificationCenter"; import _ from "lodash"; +const getSeverityCount = (count_by_severity_level, severity) => { + return count_by_severity_level.find((item) => item.severity === severity)?.count || 0 +} + + +const NavbarNotificationIcon = () => { + + const { data } = useGetEventsSummaryQuery() + const count_by_severity_level = data?.count_by_severity_level || [] + + const currentTopSeverity = getSeverityCount(count_by_severity_level, SEVERITY.ERROR) > 0 + ? SEVERITY.ERROR : + getSeverityCount(count_by_severity_level, SEVERITY.WARNING) > 0 ? SEVERITY.WARNING : null + const currentSeverityStyle = currentTopSeverity ? SEVERITY_STYLE[currentTopSeverity] : null + const topSeverityCount = getSeverityCount(count_by_severity_level, currentTopSeverity) + const classes = useNavNotificationIconStyles({ + badgeColor: currentSeverityStyle?.color + }) + if (currentTopSeverity) { + return ( + + + + ) + } + return ( + + ) +} + const NotificationCountChip = ({ classes, notificationStyle, count, handleClick }) => { const chipStyles = { @@ -44,10 +74,6 @@ const Header = ({ handleFilter }) => { total_count: 0 } const classes = useStyles() - const getSeverityCount = (severity) => { - return count_by_severity_level.find((item) => item.severity === severity)?.count || 0 - } - const onClickSeverity = (severity) => { handleFilter({ severity: [severity] @@ -60,8 +86,8 @@ const Header = ({ handleFilter }) => { }) } - const archivedCount = count_by_severity_level - .reduce((acc, item) => acc + item.count, 0) - total_count + const archivedCount = total_count - count_by_severity_level + .reduce((acc, item) => acc + item.count, 0) return (
@@ -74,7 +100,7 @@ const Header = ({ handleFilter }) => { {Object.values(SEVERITY).map(severity => ( onClickSeverity(severity)} notificationStyle={SEVERITY_STYLE[severity]} - count={getSeverityCount(severity)} />) + count={getSeverityCount(count_by_severity_level, severity)} />) )} { setAnchorEl(null); }} > - - {/* - */} +
diff --git a/ui/components/NotificationCenter/notificationCenter.style.js b/ui/components/NotificationCenter/notificationCenter.style.js index a289ef1c1ed..b6fc31964bc 100644 --- a/ui/components/NotificationCenter/notificationCenter.style.js +++ b/ui/components/NotificationCenter/notificationCenter.style.js @@ -77,3 +77,11 @@ export const useStyles = makeStyles((theme) => ({ }, })); + +export const useNavNotificationIconStyles = makeStyles(() => ({ + root: (props) => ({ + '& .MuiBadge-badge': { + backgroundColor: props.badgeColor, + }, + }), +})) diff --git a/ui/themes/app.js b/ui/themes/app.js index a298ec9c1f2..570070c9e47 100644 --- a/ui/themes/app.js +++ b/ui/themes/app.js @@ -512,7 +512,7 @@ export const styles = (theme) => ({ color: `${notificationColors.info} !important`, pointerEvents: "auto !important" }, notifWarn: { - backgroundColor: "rgba(240, 163, 3, 0.04) !important", + backgroundColor: "#fff !important", color: `${notificationColors.warning} !important`, pointerEvents: "auto !important" }, notifError: { From 8bc8b5cae8951fe6403af59d4922bde437ddebf9 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Wed, 13 Sep 2023 16:33:19 +0530 Subject: [PATCH 08/44] lets rem it Signed-off-by: aabidsofi19 --- .../NotificationCenter/notification.js | 21 +++++++++---------- .../notificationCenter.style.js | 14 ++++++------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js index 9738302c1d7..2b886fc96cb 100644 --- a/ui/components/NotificationCenter/notification.js +++ b/ui/components/NotificationCenter/notification.js @@ -18,14 +18,14 @@ import { changeEventStatus, deleteEvent } from '../../store/slices/events'; const useStyles = makeStyles(() => ({ root: (props) => ({ width: "100%", - borderRadius: "3px", - border: `1px solid ${props.notificationColor}`, - borderLeftWidth: props.status === STATUS.READ ? "4px" : "1px", - marginBlock: "8px", + borderRadius: "0.25rem", + border: `0.1rem solid ${props.notificationColor}`, + borderLeftWidth: props.status === STATUS.READ ? "0.25rem" : "0.1rem", + marginBlock: "0.5rem", }), summary: (props) => ({ - paddingBlock: "8px", + paddingBlock: "0,5rem", cursor: "pointer", backgroundColor: alpha(props.notificationColor, 0.20), }), @@ -46,7 +46,7 @@ const useStyles = makeStyles(() => ({ width: "100%", }, expanded: { - paddingBlock: "12px", + paddingBlock: "0.75rem", }, actorAvatar: { display: "flex", @@ -55,7 +55,6 @@ const useStyles = makeStyles(() => ({ }, descriptionHeading: { - fontSize: "16px", fontWeight: "bolder !important", textTransform: "uppercase", }, @@ -68,10 +67,10 @@ const useMenuStyles = makeStyles((theme) => { paper: { color: theme.palette.secondary.iconMain, boxShadow: theme.shadows[4], - borderRadius: "3px", + borderRadius: "0.25", paddingInline: "0.5rem", paddingBlock: "0.25rem", - width: "200px", + width: "12.5rem", }, list: { @@ -80,7 +79,7 @@ const useMenuStyles = makeStyles((theme) => { flexDirection: "column", gridGap: "0.5rem", marginBlock: "0.5rem", - borderRadius: "4px", + borderRadius: "0.25rem", backgroundColor: theme.palette.secondary.honeyComb, }, @@ -223,7 +222,7 @@ export const ChangeStatus = ({ event }) => { } const BulletList = ({ items }) => { - return
    + return
      {[items].map((i) =>
    1. {i}
    2. )} diff --git a/ui/components/NotificationCenter/notificationCenter.style.js b/ui/components/NotificationCenter/notificationCenter.style.js index b6fc31964bc..2649e8d813a 100644 --- a/ui/components/NotificationCenter/notificationCenter.style.js +++ b/ui/components/NotificationCenter/notificationCenter.style.js @@ -32,7 +32,7 @@ export const useStyles = makeStyles((theme) => ({ }, container: { - padding: "20px" + padding: "1.25rem" }, header: { display: "flex", @@ -47,8 +47,8 @@ export const useStyles = makeStyles((theme) => ({ gap: "0.5rem", }, titleBellIcon: { - width: "36px", - height: "36px", + width: "2.25rem", + height: "2.25rem", borderRadius: "100%", backgroundColor: "black", display: "flex", @@ -57,18 +57,18 @@ export const useStyles = makeStyles((theme) => ({ alignItems: "center" }, severityChip: { - borderRadius: "4px", + borderRadius: "0.25rem", display: "flex", - gap: "4px", + gap: "0.25rem", justifyContent: "start", alignItems: "center", - fontSize: "16px", + fontSize: "1rem", cursor: "pointer", }, severityChips: { display: "flex", - gap: "12px", + gap: "0.75rem", alignItems: "center", }, From 82718e9de666559000415a85b986f6a90a864ded Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Wed, 13 Sep 2023 17:09:42 +0530 Subject: [PATCH 09/44] fix mobile usability issues Signed-off-by: aabidsofi19 --- ui/components/NotificationCenter/index.js | 6 +++--- .../NotificationCenter/notification.js | 17 ++++++++++------- .../notificationCenter.style.js | 5 +++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index bd6a3ff4be0..a8467fc9190 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -66,7 +66,7 @@ const NotificationCountChip = ({ classes, notificationStyle, count, handleClick ) } -const Header = ({ handleFilter }) => { +const Header = ({ handleFilter , handleClose }) => { const { data } = useGetEventsSummaryQuery(); const { count_by_severity_level, total_count } = data || { @@ -91,7 +91,7 @@ const Header = ({ handleFilter }) => { return (
      -
      +
      Notifications @@ -279,7 +279,7 @@ const MesheryNotification = () => {
      -
      +
      diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js index 2b886fc96cb..4114487d97f 100644 --- a/ui/components/NotificationCenter/notification.js +++ b/ui/components/NotificationCenter/notification.js @@ -1,10 +1,10 @@ import * as React from 'react'; -import { Box, Button, Collapse, Divider, Grid, IconButton, Popover, Typography, alpha, useTheme } from "@material-ui/core" +import { Box, Button, Collapse, Divider, Grid, Hidden, IconButton, Popover, Typography, alpha, useTheme } from "@material-ui/core" import { makeStyles } from "@material-ui/core" import { SEVERITY_STYLE, STATUS } from "./constants" import { iconLarge, iconMedium } from "../../css/icons.styles" import { MoreVert } from "@material-ui/icons" -import { Avatar } from "@mui/material" +// import { Avatar } from "@mui/material" import FacebookIcon from "../../assets/icons/FacebookIcon" import LinkedInIcon from "../../assets/icons/LinkedInIcon" import TwitterIcon from "../../assets/icons/TwitterIcon" @@ -47,6 +47,7 @@ const useStyles = makeStyles(() => ({ }, expanded: { paddingBlock: "0.75rem", + paddingInline: "0.2rem", }, actorAvatar: { display: "flex", @@ -252,12 +253,14 @@ export const Notification = ({ event }) => { - + {event.description} - - {formatTimestamp(event.created_at)} - + + + {formatTimestamp(event.created_at)} + + @@ -267,7 +270,7 @@ export const Notification = ({ event }) => { - S + diff --git a/ui/components/NotificationCenter/notificationCenter.style.js b/ui/components/NotificationCenter/notificationCenter.style.js index 2649e8d813a..848dec8cf6b 100644 --- a/ui/components/NotificationCenter/notificationCenter.style.js +++ b/ui/components/NotificationCenter/notificationCenter.style.js @@ -3,7 +3,7 @@ import { makeStyles } from "@material-ui/core" export const useStyles = makeStyles((theme) => ({ sidelist: { width: "45rem", - maxWidth: "90vw", + maxWidth: "95vw", }, notificationButton: { height: "100%" }, notificationDrawer: { @@ -54,7 +54,8 @@ export const useStyles = makeStyles((theme) => ({ display: "flex", padding: "0.2rem", justifyContent: "center", - alignItems: "center" + alignItems: "center", + cursor:"pointer" }, severityChip: { borderRadius: "0.25rem", From 414c3a4b4db384212660f13c3c1ee89c8d19de16 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Wed, 13 Sep 2023 18:09:24 +0530 Subject: [PATCH 10/44] Add Empty State and loading state Signed-off-by: aabidsofi19 --- ui/assets/icons/DoneIcon.js | 21 +++++++++++++++ ui/components/NotificationCenter/index.js | 32 +++++++++++++++++++---- ui/store/slices/events.js | 1 - ui/utils/hooks/useNotification.js | 1 - 4 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 ui/assets/icons/DoneIcon.js diff --git a/ui/assets/icons/DoneIcon.js b/ui/assets/icons/DoneIcon.js new file mode 100644 index 00000000000..627de9760cf --- /dev/null +++ b/ui/assets/icons/DoneIcon.js @@ -0,0 +1,21 @@ +import React from 'react'; + +export const DoneIcon = ({ width, height, fill, style = {} }) => { + + return ( + + + + + + + + + + + + ) +} + + +export default DoneIcon \ No newline at end of file diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index a8467fc9190..99f635f1f5d 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import IconButton from "@material-ui/core/IconButton"; import { Provider, useDispatch, useSelector } from "react-redux"; import NoSsr from "@material-ui/core/NoSsr"; -import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha, Chip, Button, Badge } from "@material-ui/core"; +import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha, Chip, Button, Badge, CircularProgress, Box, useTheme } from "@material-ui/core"; import Filter from "./filter"; import BellIcon from "../../assets/icons/BellIcon.js" import { iconMedium } from "../../css/icons.styles"; @@ -14,12 +14,21 @@ import { useNavNotificationIconStyles, useStyles } from "./notificationCenter.st import { closeNotificationCenter, loadEvents, loadNextPage, selectEvents, toggleNotificationCenter } from "../../store/slices/events"; import { useGetEventsSummaryQuery, useLazyGetEventsQuery } from "../../rtk-query/notificationCenter"; import _ from "lodash"; +import DoneIcon from "../../assets/icons/DoneIcon"; const getSeverityCount = (count_by_severity_level, severity) => { return count_by_severity_level.find((item) => item.severity === severity)?.count || 0 } +const EmptyState = () => { + const theme = useTheme().palette.secondary + return ( + + + No notifications to show + ) +} const NavbarNotificationIcon = () => { @@ -66,7 +75,7 @@ const NotificationCountChip = ({ classes, notificationStyle, count, handleClick ) } -const Header = ({ handleFilter , handleClose }) => { +const Header = ({ handleFilter, handleClose }) => { const { data } = useGetEventsSummaryQuery(); const { count_by_severity_level, total_count } = data || { @@ -112,6 +121,12 @@ const Header = ({ handleFilter , handleClose }) => { ) } +const Loading = () => { + return ( + + + ) +} const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { @@ -120,6 +135,9 @@ const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { const lastEventRef = useRef(null) const intersectionObserver = useRef(new IntersectionObserver((entries) => { + if (isLoading && !hasMore) { + return + } const firstEntry = entries[0] if (firstEntry.isIntersecting) { handleLoadNextPage() @@ -145,9 +163,13 @@ const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { {events.map((event, idx) =>
      )} - {!isLoading && hasMore && -
      } - {isLoading &&
      Loading...
      } + + {events.length === 0 && } + +
      + {!Loading && hasMore && } +
      + {isLoading && } ) } diff --git a/ui/store/slices/events.js b/ui/store/slices/events.js index 42cad6f32e3..b0290ed30c6 100644 --- a/ui/store/slices/events.js +++ b/ui/store/slices/events.js @@ -99,7 +99,6 @@ export default eventsSlice.reducer export const loadEvents = (fetch, page, filters) => async (dispatch, getState) => { const currentView = getState().events.current_view - try { const { data } = await fetch({ page, filters }) dispatch(setCurrentView({ diff --git a/ui/utils/hooks/useNotification.js b/ui/utils/hooks/useNotification.js index 39a49c91a37..cea528fb4b2 100644 --- a/ui/utils/hooks/useNotification.js +++ b/ui/utils/hooks/useNotification.js @@ -22,7 +22,6 @@ import { toggleNotificationCenter } from "../../store/slices/events"; */ export const useNotification = () => { const x = useSnackbar() - console.log("useSnackbar", x) const { enqueueSnackbar, closeSnackbar } = useSnackbar() From fbc0c21550046cca028bc6f08ed7ecfb8dec07f7 Mon Sep 17 00:00:00 2001 From: Pranav Singh Date: Thu, 14 Sep 2023 22:06:11 +0530 Subject: [PATCH 11/44] Update healthcheck.go Signed-off-by: Pranav Singh --- mesheryctl/pkg/utils/healthcheck.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesheryctl/pkg/utils/healthcheck.go b/mesheryctl/pkg/utils/healthcheck.go index d2f797d7b38..7a302ce9a65 100644 --- a/mesheryctl/pkg/utils/healthcheck.go +++ b/mesheryctl/pkg/utils/healthcheck.go @@ -75,7 +75,7 @@ func getK8sVersion(versionString string) ([3]int, error) { // CheckKubectlVersion validates whether the installed kubectl version is // running a minimum kubectl version. func CheckKubectlVersion() error { - cmd := exec.Command("kubectl", "version", "--client", "--short") + cmd := exec.Command("kubectl", "version") bytes, err := cmd.Output() if err != nil { return err From 20b2b4a145ad49f4e00412c56b2d89a2b69d59bf Mon Sep 17 00:00:00 2001 From: MUzairS15 Date: Fri, 15 Sep 2023 01:32:18 +0530 Subject: [PATCH 12/44] format k8s response Signed-off-by: MUzairS15 --- server/handlers/design_engine_handler.go | 96 +++++++++++--------- server/helpers/utils/utils.go | 14 +++ server/models/pattern/core/pattern.go | 2 +- server/models/pattern/patterns/k8s/error.go | 67 ++++++++++++++ server/models/pattern/patterns/k8s/k8s.go | 74 ++++++++++----- server/models/pattern/patterns/patterns.go | 7 +- server/models/pattern/stages/svc_provider.go | 2 +- ui/components/DryRun/DryRunComponent.js | 2 +- 8 files changed, 193 insertions(+), 71 deletions(-) create mode 100644 server/models/pattern/patterns/k8s/error.go diff --git a/server/handlers/design_engine_handler.go b/server/handlers/design_engine_handler.go index 423a7dc34f2..1300ecf6f95 100644 --- a/server/handlers/design_engine_handler.go +++ b/server/handlers/design_engine_handler.go @@ -12,6 +12,7 @@ import ( "github.com/ghodss/yaml" "github.com/gofrs/uuid" + "github.com/layer5io/meshery/server/helpers/utils" "github.com/layer5io/meshery/server/meshes" "github.com/layer5io/meshery/server/models" "github.com/layer5io/meshery/server/models/pattern/core" @@ -345,7 +346,7 @@ func (sap *serviceActionProvider) Mutate(p *core.Pattern) { // NOTE: Currently tied to kubernetes // Returns ComponentName->ContextID->Response -func (sap *serviceActionProvider) DryRun(comps []v1alpha1.Component) (resp map[string]map[string]core.DryRunResponse2, err error) { +func (sap *serviceActionProvider) DryRun(comps []v1alpha1.Component) (resp map[string]map[string]core.DryRunResponseWrapper, err error) { for _, cmp := range comps { for ctxID, kc := range sap.ctxTokubeconfig { cl, err := meshkube.New([]byte(kc)) @@ -353,8 +354,9 @@ func (sap *serviceActionProvider) DryRun(comps []v1alpha1.Component) (resp map[s return resp, err } - st, ok, err := k8s.DryRunHelper(cl, cmp) - dResp := core.DryRunResponse2{Success: ok, Component: &core.Service{ + // status represents kubernetes status object + status, ok, err := k8s.DryRunHelper(cl, cmp) + dResp := core.DryRunResponseWrapper{Success: ok, Component: &core.Service{ Name: cmp.Name, Type: cmp.Spec.Type, Namespace: cmp.Namespace, @@ -364,10 +366,11 @@ func (sap *serviceActionProvider) DryRun(comps []v1alpha1.Component) (resp map[s Labels: cmp.Labels, Annotations: cmp.Annotations, }} + // Dry run was success if ok { dResp.Component.Settings = make(map[string]interface{}) - for k, v := range st { + for k, v := range status { if k == "apiVersion" || k == "kind" || k == "metadata" { continue } @@ -377,63 +380,66 @@ func (sap *serviceActionProvider) DryRun(comps []v1alpha1.Component) (resp map[s dResp.Error = &core.DryRunResponse{ Status: err.Error(), } - } else { //Dry run failure returned with an error wrapped in kubernetes custom error - dResp.Error = &core.DryRunResponse{} - byt, err := json.Marshal(st) - if err != nil { - return nil, err - } - var a v1.StatusApplyConfiguration - err = json.Unmarshal(byt, &a) + } else { //Dry run failure returned with an error wrapped in kubernetes custom error + dResp.Error, err = convertRawDryRunResponse(cmp.Name, status) if err != nil { return nil, err } - if a.Status != nil { - dResp.Error.Status = *a.Status - } - dResp.Error.Causes = make([]core.DryRunFailureCause, 0) - if a.Details != nil { - for _, c := range a.Details.Causes { - msg := "" - field := "" - typ := "" - if c.Message != nil { - msg = *c.Message - } - if c.Field != nil { - field = cmp.Name + "." + getComponentFieldPathFromK8sFieldPath(*c.Field) - } - if c.Type != nil { - typ = string(*c.Type) - } - failureCase := core.DryRunFailureCause{Message: msg, FieldPath: field, Type: typ} - dResp.Error.Causes = append(dResp.Error.Causes, failureCase) - } - } } if resp == nil { - resp = make(map[string]map[string]core.DryRunResponse2) + resp = make(map[string]map[string]core.DryRunResponseWrapper) } if resp[cmp.Name] == nil { - resp[cmp.Name] = make(map[string]core.DryRunResponse2) + resp[cmp.Name] = make(map[string]core.DryRunResponseWrapper) } resp[cmp.Name][ctxID] = dResp } } return } -func getComponentFieldPathFromK8sFieldPath(path string) (newpath string) { - if strings.HasPrefix(path, "metadata.") { - path = strings.TrimPrefix(path, "metadata.") - paths := strings.Split(path, ".") - if len(paths) != 0 { - if paths[0] == "name" || paths[0] == "namespace" || paths[0] == "labels" || paths[0] == "annotations" { - return paths[0] + +func convertRawDryRunResponse(componentName string, status map[string]interface{}) (*core.DryRunResponse, error) { + response := core.DryRunResponse{} + + byt, err := json.Marshal(status) + if err != nil { + return nil, err + } + + var a v1.StatusApplyConfiguration + err = json.Unmarshal(byt, &a) + if err != nil { + return nil, err + } + + if a.Status != nil { + response.Status = *a.Status + } + + response.Causes = make([]core.DryRunFailureCause, 0) + if a.Details != nil { + for _, cause := range a.Details.Causes { + msg := "" + field := "" + typ := "" + if cause.Message != nil { + msg = *cause.Message + } + if cause.Field != nil { + field = componentName + "." + utils.GetComponentFieldPathFromK8sFieldPath(*cause.Field) + } + if cause.Type != nil { + typ = string(*cause.Type) } + failureCase := core.DryRunFailureCause{Message: msg, FieldPath: field, Type: typ} + response.Causes = append(response.Causes, failureCase) } - return } - return fmt.Sprintf("%s.%s", "settings", path) + + if len(response.Causes) == 0 && a.Message != nil { + response.Status = *a.Message + } + return &response, nil } func (sap *serviceActionProvider) Provision(ccp stages.CompConfigPair) (string, error) { // Marshal the component diff --git a/server/helpers/utils/utils.go b/server/helpers/utils/utils.go index 50fca08fea5..4e82948fa59 100644 --- a/server/helpers/utils/utils.go +++ b/server/helpers/utils/utils.go @@ -307,3 +307,17 @@ func SanitizeFileName(fileName string) string { finalPath = append(finalPath, "-*.", suffixPath) return strings.Join(finalPath, "") } + +func GetComponentFieldPathFromK8sFieldPath(path string) (newpath string) { + if strings.HasPrefix(path, "metadata.") { + path = strings.TrimPrefix(path, "metadata.") + paths := strings.Split(path, ".") + if len(paths) != 0 { + if paths[0] == "name" || paths[0] == "namespace" || paths[0] == "labels" || paths[0] == "annotations" { + return paths[0] + } + } + return + } + return fmt.Sprintf("%s.%s", "settings", path) +} diff --git a/server/models/pattern/core/pattern.go b/server/models/pattern/core/pattern.go index 7433d337042..087ee842c3a 100644 --- a/server/models/pattern/core/pattern.go +++ b/server/models/pattern/core/pattern.go @@ -124,7 +124,7 @@ func isSpecialKey(k string) bool { // In case of any breaking change or bug caused by this, set this to false and the whitespace addition in schema generated/consumed would be removed(will go back to default behavior) const Format prettifier = true -type DryRunResponse2 struct { +type DryRunResponseWrapper struct { //When success is true, error will be nil and Component will contain the structure of the component as it will look after deployment //When success is false, error will contain the errors. And Component will be set to Nil Success bool `json:"success"` diff --git a/server/models/pattern/patterns/k8s/error.go b/server/models/pattern/patterns/k8s/error.go new file mode 100644 index 00000000000..ca25fae5a7a --- /dev/null +++ b/server/models/pattern/patterns/k8s/error.go @@ -0,0 +1,67 @@ +package k8s + +import ( + "encoding/json" + "fmt" + meshkitutils "github.com/layer5io/meshkit/utils/kubernetes" + + "github.com/layer5io/meshery/server/helpers/utils" + "github.com/layer5io/meshkit/errors" + kubeerror "k8s.io/apimachinery/pkg/api/errors" +) + +const ( + ErrDryRunCode = "replace_me" +) + +func isErrKubeStatusErr(err error) bool { + switch err.(type) { + case *kubeerror.StatusError: + return true + default: + return false + } +} + +func formatKubeStatusErrToMeshkitErr(status *[]byte, componentName string) error { + var shortDescription, longDescription []string + + var kubeErr kubeerror.StatusError + + err := json.Unmarshal(*status, &kubeErr) + kubeStatus := kubeErr.ErrStatus + if err != nil { + return err + } + var st string + + st = string(kubeStatus.Status) + + if kubeStatus.Details != nil { + + sd := kubeStatus.Details.Kind + + sd = fmt.Sprintf("%s %s", sd, st) + + sd = fmt.Sprintf("%s %s", sd, kubeStatus.Details.Name) + + for _, cause := range kubeStatus.Details.Causes { + var shortDes, longDes, field string + longDes = cause.Message + longDescription = append(longDescription, longDes) + sd = fmt.Sprintf("%s %s", sd, cause.Type) + field = componentName + "." + utils.GetComponentFieldPathFromK8sFieldPath(cause.Field) + shortDes = fmt.Sprintf("%s: %s", sd, field) + shortDescription = append(shortDescription, shortDes) + + } + if len(kubeStatus.Details.Causes) == 0 { + longDescription = append(longDescription, kubeStatus.Message) + } + } + return errors.New(meshkitutils.ErrApplyManifestCode, errors.Alert, shortDescription, longDescription, []string{}, []string{}) +} + +func ErrDryRun(err error, obj string) error { + return errors.New(ErrDryRunCode, errors.Alert, []string{"error running dry run on the design"}, []string{err.Error()}, []string{obj}, []string{}) +} diff --git a/server/models/pattern/patterns/k8s/k8s.go b/server/models/pattern/patterns/k8s/k8s.go index 6d5b998ba61..811c2aa28dc 100644 --- a/server/models/pattern/patterns/k8s/k8s.go +++ b/server/models/pattern/patterns/k8s/k8s.go @@ -6,23 +6,36 @@ import ( "fmt" "strings" + "github.com/layer5io/meshery/server/models" "github.com/layer5io/meshkit/models/oam/core/v1alpha1" meshkube "github.com/layer5io/meshkit/utils/kubernetes" "gopkg.in/yaml.v2" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/rest" ) -func Deploy(kubeClient *meshkube.Client, oamComp v1alpha1.Component, _ v1alpha1.Configuration, isDel bool) error { - resource := createK8sResourceStructure(oamComp) +func Deploy(kubeClient *meshkube.Client, comp v1alpha1.Component, _ v1alpha1.Configuration, isDel bool) error { + resource := createK8sResourceStructure(comp) manifest, err := yaml.Marshal(resource) if err != nil { return err } - return kubeClient.ApplyManifest(manifest, meshkube.ApplyOptions{ - Namespace: oamComp.Namespace, + err = kubeClient.ApplyManifest(manifest, meshkube.ApplyOptions{ + Namespace: comp.Namespace, Update: true, Delete: isDel, }) + if err != nil { + fmt.Println("29: ", err) + if isErrKubeStatusErr(err) { + status, _ := json.Marshal(err) + fmt.Println("31: ", string(status)) + return formatKubeStatusErrToMeshkitErr(&status, comp.Name) + } else { + return meshkube.ErrApplyManifest(err) + } + } + return nil } func DryRunHelper(client *meshkube.Client, comp v1alpha1.Component) (st map[string]interface{}, success bool, err error) { @@ -34,46 +47,45 @@ func DryRunHelper(client *meshkube.Client, comp v1alpha1.Component) (st map[stri // TODO: add more tests for this function func dryRun(rClient rest.Interface, k8sResource map[string]interface{}, namespace string) (st map[string]interface{}, success bool, err error) { if k8sResource["kind"] == "" || k8sResource["apiVersion"] == "" { - err = fmt.Errorf("invalid resource or namespace not provided") + err = ErrDryRun(fmt.Errorf("invalid resource or namespace not provided"), "\"kind\" and \"apiVersion\" cannot be empty") return } + aV := k8sResource["apiVersion"].(string) // for non-core resources, the endpoint should use 'apis' instead of 'api' apiString := "api" if len(strings.Split(aV, "/")) > 1 { apiString = apiString + "s" } + var path string + if namespace != "" { path = fmt.Sprintf("/%s/%s/namespaces/%s/%s", apiString, aV, namespace, kindToResource(k8sResource["kind"].(string))) } else { path = fmt.Sprintf("/%s/%s/%s", apiString, aV, kindToResource(k8sResource["kind"].(string))) } + data, err := json.Marshal(k8sResource) if err != nil { + err = models.ErrMarshal(err, "k8s resource") return } + req := rClient.Post().AbsPath(path).Body(data).SetHeader("Content-Type", "application/json").SetHeader("Accept", "application/json").Param("dryRun", "All").Param("fieldValidation", "Strict").Param("fieldManager", "meshery") res := req.Do(context.Background()) - if res.Error() != nil { - err = res.Error() - return - } + // ignoring the error since this client-go treats failure of dryRun as an error - resp, _ := res.Raw() - e := json.Unmarshal(resp, &st) - if e != nil { - err = fmt.Errorf("cannot serialize Status object from the server: %s", e.Error()) - return - } - if st == nil || st["kind"] == nil { - err = fmt.Errorf("nil response for dryRun from kubernetes") - } - if st["status"] == "Failure" { //The dryRun returned errors in the form of Status - success = false - return + resp, err := res.Raw() + fmt.Println("\n\n\n", string(resp)) + switch err.(type) { + case *errors.StatusError: + st, success, err = formatDryRunResponse(resp, err) + case *errors.UnexpectedObjectError: + st, success, err = formatDryRunResponse(resp, err) + default: + return } - success = true return } @@ -104,3 +116,21 @@ func createK8sResourceStructure(comp v1alpha1.Component) map[string]interface{} } return component } + +func formatDryRunResponse(resp []byte, err error) (status map[string]interface{}, success bool, meshkiterr error) { + + e := json.Unmarshal(resp, &status) + if e != nil { + meshkiterr = models.ErrMarshal(err, fmt.Sprintf("cannot serialize Status object from the server: %s", e.Error())) + return + } + if status == nil || status["kind"] == nil { + meshkiterr = ErrDryRun(fmt.Errorf("nil response for dryRun from kubernetes"), "") + } + if status["status"] == "Failure" { // The dryRun returns errors in the form of Status + success = false + return + } + success = true + return +} \ No newline at end of file diff --git a/server/models/pattern/patterns/patterns.go b/server/models/pattern/patterns/patterns.go index c1a35a30620..b05f881465f 100644 --- a/server/models/pattern/patterns/patterns.go +++ b/server/models/pattern/patterns/patterns.go @@ -77,7 +77,8 @@ func ProcessOAM(kconfigs []string, oamComps []string, oamConfig string, isDel bo evt := events.NewEvent().FromSystem(*mesheryInstanceID).WithSeverity(events.Alert).WithCategory("event").WithAction("persist").WithDescription("Failed persisting events").FromUser(userUUID).Build() go ec.Publish(userUUID, evt) } - + go ec.Publish(userUUID, event) + continue } var description string @@ -127,6 +128,7 @@ func ProcessOAM(kconfigs []string, oamComps []string, oamConfig string, isDel bo evt := events.NewEvent().FromSystem(*mesheryInstanceID).WithSeverity(events.Alert).WithCategory("event").WithAction("persist").WithDescription("Failed persisting events").FromUser(userUUID).Build() go ec.Publish(userUUID, evt) } + go ec.Publish(userUUID, event) } //All other components will be handled directly by Kubernetes //TODO: Add a Mapper utility function which carries the logic for X hosts can handle Y components under Z circumstances. @@ -151,6 +153,8 @@ func ProcessOAM(kconfigs []string, oamComps []string, oamConfig string, isDel bo evt := events.NewEvent().FromSystem(*mesheryInstanceID).WithSeverity(severity).WithCategory("event").WithAction("persist").WithDescription("Failed persisting events").FromUser(userUUID).Build() go ec.Publish(userUUID, evt) } + + go ec.Publish(userUUID, event) continue } if !isDel { @@ -166,6 +170,7 @@ func ProcessOAM(kconfigs []string, oamComps []string, oamConfig string, isDel bo evt := events.NewEvent().FromSystem(*mesheryInstanceID).WithSeverity(events.Alert).WithCategory("event").WithAction("persist").WithDescription("Failed persisting events").FromUser(userUUID).Build() go ec.Publish(userUUID, evt) } + go ec.Publish(userUUID, event) } }(kcli) } diff --git a/server/models/pattern/stages/svc_provider.go b/server/models/pattern/stages/svc_provider.go index efc05dcb260..5c098684604 100644 --- a/server/models/pattern/stages/svc_provider.go +++ b/server/models/pattern/stages/svc_provider.go @@ -25,6 +25,6 @@ type ServiceActionProvider interface { Provision(CompConfigPair) (string, error) GetRegistry() *meshmodel.RegistryManager Persist(string, core.Service, bool) error - DryRun([]v1alpha1.Component) (map[string]map[string]core.DryRunResponse2, error) + DryRun([]v1alpha1.Component) (map[string]map[string]core.DryRunResponseWrapper, error) Mutate(*core.Pattern) //Uses pre-defined policies/configuration to mutate the pattern } diff --git a/ui/components/DryRun/DryRunComponent.js b/ui/components/DryRun/DryRunComponent.js index 3911ba340a5..f5722b7481a 100644 --- a/ui/components/DryRun/DryRunComponent.js +++ b/ui/components/DryRun/DryRunComponent.js @@ -193,7 +193,7 @@ const ExpandableComponentErrors = withStyles(styles)(({ export const dryRunAndFormatErrors = (design,selectedContexts) => { function getErrors(error) { - if (error?.Causes) { + if (error?.Causes && error?.Causes.length > 0) { return error.Causes; } // if causes aren't present use the status From ceceb4334948fa557a147fe7b1222cceeef44671 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Fri, 15 Sep 2023 01:51:17 +0530 Subject: [PATCH 13/44] added avatar ui Signed-off-by: aabidsofi19 --- ui/components/NotificationCenter/constants.js | 30 +-- ui/components/NotificationCenter/filter.js | 230 +++++++++--------- ui/components/NotificationCenter/index.js | 42 ++-- .../NotificationCenter/notification.js | 140 ++++++----- .../notificationCenter.style.js | 124 +++++----- ui/components/User.js | 2 +- ui/pages/_app.js | 178 +++++++------- ui/rtk-query/user.js | 17 ++ 8 files changed, 394 insertions(+), 369 deletions(-) create mode 100644 ui/rtk-query/user.js diff --git a/ui/components/NotificationCenter/constants.js b/ui/components/NotificationCenter/constants.js index 0510e90e2e6..f63b8b0b7a0 100644 --- a/ui/components/NotificationCenter/constants.js +++ b/ui/components/NotificationCenter/constants.js @@ -5,9 +5,9 @@ import ErrorIcon from "../../assets/icons/ErrorIcon.js" import { Colors } from "../../themes/app"; export const SEVERITY = { - INFO: "informational", - ERROR: "error", - WARNING: "warning", + INFO : "informational", + ERROR : "error", + WARNING : "warning", // SUCCESS: "success" } @@ -17,24 +17,24 @@ export const STATUS = { } export const STATUS_STYLE = { - [STATUS.READ]: { - icon: ArchiveIcon, - color: Colors.charcoal + [STATUS.READ] : { + icon : ArchiveIcon, + color : Colors.charcoal } } export const SEVERITY_STYLE = { - [SEVERITY.INFO]: { - icon: ErrorIcon, - color: NOTIFICATIONCOLORS.INFO + [SEVERITY.INFO] : { + icon : ErrorIcon, + color : NOTIFICATIONCOLORS.INFO }, - [SEVERITY.ERROR]: { - icon: ErrorIcon, - color: NOTIFICATIONCOLORS.ERROR + [SEVERITY.ERROR] : { + icon : ErrorIcon, + color : NOTIFICATIONCOLORS.ERROR }, - [SEVERITY.WARNING]: { - icon: AlertIcon, - color: NOTIFICATIONCOLORS.WARNING + [SEVERITY.WARNING] : { + icon : AlertIcon, + color : NOTIFICATIONCOLORS.WARNING }, } \ No newline at end of file diff --git a/ui/components/NotificationCenter/filter.js b/ui/components/NotificationCenter/filter.js index e17a2a36cd3..45ccee0009e 100644 --- a/ui/components/NotificationCenter/filter.js +++ b/ui/components/NotificationCenter/filter.js @@ -19,112 +19,112 @@ import clsx from "clsx"; import { SEVERITY, STATUS } from "./constants"; const useStyles = makeStyles((theme) => ({ - root: { - position: "relative", - backgroundColor: theme.palette.secondary.elevatedComponents, + root : { + position : "relative", + backgroundColor : theme.palette.secondary.elevatedComponents, }, - input: { - width: "100%", - "& .MuiOutlinedInput-root": { - borderRadius: "6px", - backgroundColor: theme.palette.secondary.searchBackground, - "& fieldset": { - borderRadius: "6px", - border: `2px solid ${theme.palette.secondary.searchBorder}`, + input : { + width : "100%", + "& .MuiOutlinedInput-root" : { + borderRadius : "6px", + backgroundColor : theme.palette.secondary.searchBackground, + "& fieldset" : { + borderRadius : "6px", + border : `2px solid ${theme.palette.secondary.searchBorder}`, }, }, }, - dropDown: { - backgroundColor: theme.palette.secondary.searchBackground, - borderRadius: "6px", - boxShadow: + dropDown : { + backgroundColor : theme.palette.secondary.searchBackground, + borderRadius : "6px", + boxShadow : "0px 2px 4px 0px rgba(0, 0, 0, 0.20), 0px 1px 10px 0px rgba(0, 0, 0, 0.12), 0px 4px 5px 0px rgba(0, 0, 0, 0.14)", - border: `2px solid ${theme.palette.secondary.searchBorder}`, - marginTop: "0.2rem", + border : `2px solid ${theme.palette.secondary.searchBorder}`, + marginTop : "0.2rem", }, })); const useFilterStyles = makeStyles((theme) => ({ - item: { - fontFamily: "Qanelas Soft, sans-serif", - display: "flex", - gap: "0.3rem", - margin: "0.3rem", - padding: "0.3rem", - paddingInline: "1rem", - borderRadius: "6px", - cursor: "pointer", - "&:hover": { - backgroundColor: alpha(theme.palette.secondary.link2, 0.25), + item : { + fontFamily : "Qanelas Soft, sans-serif", + display : "flex", + gap : "0.3rem", + margin : "0.3rem", + padding : "0.3rem", + paddingInline : "1rem", + borderRadius : "6px", + cursor : "pointer", + "&:hover" : { + backgroundColor : alpha(theme.palette.secondary.link2, 0.25), }, }, - label: { - fontWeight: 500, - color: theme.palette.secondary.icon, + label : { + fontWeight : 500, + color : theme.palette.secondary.icon, }, - description: { - fontWeight: 400, - color: theme.palette.secondary.number, + description : { + fontWeight : 400, + color : theme.palette.secondary.number, }, })); const FILTERS = { - SEVERITY: { - value: "severity", - label: "Severity", - description: "Filter by severity", - values: Object.values(SEVERITY), + SEVERITY : { + value : "severity", + label : "Severity", + description : "Filter by severity", + values : Object.values(SEVERITY), }, - STATUS: { - value: "status", - label: "Status", - description: "Filter by status", - values: Object.values(STATUS), - type: "string" + STATUS : { + value : "status", + label : "Status", + description : "Filter by status", + values : Object.values(STATUS), + type : "string" }, - TYPE: { - value: "type", - label: "Type", - description: "Filter by type", + TYPE : { + value : "type", + label : "Type", + description : "Filter by type", }, - AUTHOR: { - value: "author", - label: "Author", - description: "Filter by any user or system", + AUTHOR : { + value : "author", + label : "Author", + description : "Filter by any user or system", }, - CATEGORY: { - value: "category", - label: "Category", - description: "Filter by category", - values: ["pattern", "connection"], + CATEGORY : { + value : "category", + label : "Category", + description : "Filter by category", + values : ["pattern", "connection"], }, }; const FILTERING_STATE = { - IDLE: "idle", - SELECTING_FILTER: "selecting_filter", - SELECTING_VALUE: "selecting_value", + IDLE : "idle", + SELECTING_FILTER : "selecting_filter", + SELECTING_VALUE : "selecting_value", }; const FILTER_EVENTS = { - START: "start", - SELECT: "select_filter", - SELECT_FILTER: "select_filter", - INPUT_CHANGE: "input_change", - SELECT_FILTER_VALUE: "select_filter_value", - CLEAR: "clear", - EXIT: "exit", + START : "start", + SELECT : "select_filter", + SELECT_FILTER : "select_filter", + INPUT_CHANGE : "input_change", + SELECT_FILTER_VALUE : "select_filter_value", + CLEAR : "clear", + EXIT : "exit", }; const Delimiter = { - FILTER: " ", - FILTER_VALUE: ":", + FILTER : " ", + FILTER_VALUE : ":", }; /** @@ -171,21 +171,21 @@ const commonReducer = (stateMachine, action) => { switch (action.type) { case FILTER_EVENTS.CLEAR: return { - state: FILTERING_STATE.SELECTING_FILTER, - context: { + state : FILTERING_STATE.SELECTING_FILTER, + context : { ...context, - value: "", - prevValue: [""], + value : "", + prevValue : [""], }, }; case FILTER_EVENTS.EXIT: return { - state: FILTERING_STATE.IDLE, - context: { + state : FILTERING_STATE.IDLE, + context : { ...context, - value: "", - prevValue: [""], + value : "", + prevValue : [""], }, }; @@ -204,11 +204,11 @@ const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { case FILTER_EVENTS.SELECT: { const newValue = nextValue(context.prevValue.at(-1), action.payload.value); // ":" is used to separate the filter and its value) return { - state: nextState, - context: { + state : nextState, + context : { ...context, - value: newValue + nextDelimiter, - prevValue: [...context.prevValue, newValue], + value : newValue + nextDelimiter, + prevValue : [...context.prevValue, newValue], }, }; } @@ -226,11 +226,11 @@ const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { if (action.payload.value == context.prevValue.at(-1)) { return { - state: prevState, - context: { + state : prevState, + context : { ...context, - prevValue: context.prevValue.slice(0, -1), - value: action.payload.value, + prevValue : context.prevValue.slice(0, -1), + value : action.payload.value, }, }; } @@ -238,20 +238,20 @@ const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { if (action.payload.value.endsWith(nextDelimiter)) { const newValue = action.payload.value; return { - state: nextState, - context: { + state : nextState, + context : { ...context, - value: action.payload.value, - prevValue: [...context.prevValue, newValue.slice(0, -1)], + value : action.payload.value, + prevValue : [...context.prevValue, newValue.slice(0, -1)], }, }; } return { state, // stay in the same state - context: { + context : { ...context, - value: action.payload.value, + value : action.payload.value, }, }; default: @@ -269,7 +269,7 @@ const filterReducer = (stateMachine, action) => { case "START": return { ...stateMachine, - state: FILTERING_STATE.SELECTING_FILTER, + state : FILTERING_STATE.SELECTING_FILTER, }; default: return stateMachine; @@ -304,8 +304,8 @@ const getCurrentFilterAndValue = (filteringState) => { const currentFilter = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[0] || ""; const currentValue = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[1] || ""; return { - filter: currentFilter, - value: currentValue, + filter : currentFilter, + value : currentValue, }; }; @@ -313,14 +313,14 @@ const Filters = ({ filterStateMachine, dispatchFilterMachine }) => { const classes = useFilterStyles(); const selectFilter = (filter) => { dispatchFilterMachine({ - type: FILTER_EVENTS.SELECT, - payload: { - value: filter, + type : FILTER_EVENTS.SELECT, + payload : { + value : filter, }, }); }; - const { filter: currentFilter } = getCurrentFilterAndValue(filterStateMachine); + const { filter : currentFilter } = getCurrentFilterAndValue(filterStateMachine); const matchingFilters = currentFilter ? Object.values(FILTERS).filter((filter) => filter.value.startsWith(currentFilter)) : Object.values(FILTERS); @@ -357,8 +357,8 @@ const FilterValueSuggestions = ({ filterStateMachine, dispatchFilterMachine }) = const selectValue = (value) => { dispatchFilterMachine({ - type: FILTER_EVENTS.SELECT, - payload: { + type : FILTER_EVENTS.SELECT, + payload : { value, }, }); @@ -400,11 +400,11 @@ const Filter = ({ handleFilter }) => { const isPopperOpen = Boolean(anchorEl); const inputFieldRef = useRef(null); const [filteringState, dispatch] = useReducer(filterReducer, { - context: { - value: "", - prevValue: [""], + context : { + value : "", + prevValue : [""], }, - state: FILTERING_STATE.IDLE, + state : FILTERING_STATE.IDLE, }); const handleFilterChange = (e) => { @@ -415,21 +415,21 @@ const Filter = ({ handleFilter }) => { if (e.target.value === "") { return dispatch({ - type: FILTER_EVENTS.CLEAR, + type : FILTER_EVENTS.CLEAR, }); } return dispatch({ - type: FILTER_EVENTS.INPUT_CHANGE, - payload: { - value: e.target.value, + type : FILTER_EVENTS.INPUT_CHANGE, + payload : { + value : e.target.value, }, }); }; const handleClear = () => { dispatch({ - type: FILTER_EVENTS.EXIT, + type : FILTER_EVENTS.EXIT, }); handleFilter({}) @@ -437,7 +437,7 @@ const Filter = ({ handleFilter }) => { const handleFocus = (e) => { setAnchorEl(e.currentTarget); - dispatch({ type: "START" }); + dispatch({ type : "START" }); }; const handleClickAway = (e) => { @@ -483,13 +483,13 @@ const Filter = ({ handleFilter }) => { onChange={handleFilterChange} onFocus={handleFocus} InputProps={{ - startAdornment: ( + startAdornment : ( {" "} {" "} ), - endAdornment: ( + endAdornment : ( {filteringState.state !== FILTERING_STATE.IDLE && ( @@ -505,7 +505,7 @@ const Filter = ({ handleFilter }) => { open={filteringState.state != FILTERING_STATE.IDLE && isPopperOpen} anchorEl={inputFieldRef.current} placement="bottom-start" - style={{ zIndex: 2000 }} + style={{ zIndex : 2000 }} transition className="mui-fixed" > @@ -516,7 +516,7 @@ const Filter = ({ handleFilter }) => {
      {filteringState.state == FILTERING_STATE.SELECTING_FILTER && ( diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index 99f635f1f5d..3e36dcf52e7 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -24,9 +24,9 @@ const getSeverityCount = (count_by_severity_level, severity) => { const EmptyState = () => { const theme = useTheme().palette.secondary return ( - + - No notifications to show + No notifications to show ) } @@ -41,7 +41,7 @@ const NavbarNotificationIcon = () => { const currentSeverityStyle = currentTopSeverity ? SEVERITY_STYLE[currentTopSeverity] : null const topSeverityCount = getSeverityCount(count_by_severity_level, currentTopSeverity) const classes = useNavNotificationIconStyles({ - badgeColor: currentSeverityStyle?.color + badgeColor : currentSeverityStyle?.color }) if (currentTopSeverity) { return ( @@ -58,13 +58,13 @@ const NavbarNotificationIcon = () => { const NotificationCountChip = ({ classes, notificationStyle, count, handleClick }) => { const chipStyles = { - fill: notificationStyle.color, - height: "20px", - width: "20px", + fill : notificationStyle.color, + height : "20px", + width : "20px", } - count = Number(count).toLocaleString('en', { useGrouping: true }) + count = Number(count).toLocaleString('en', { useGrouping : true }) return ( -
      ) @@ -206,7 +209,7 @@ export const ChangeStatus = ({ event }) => { const classes = useMenuStyles() const newStatus = event.status === STATUS.READ ? STATUS.UNREAD : STATUS.READ const [updateStatusMutation] = useUpdateStatusMutation() - + const theme = useTheme() const dispatch = useDispatch() const updateStatus = (e) => { @@ -216,7 +219,8 @@ export const ChangeStatus = ({ event }) => { return (
      ) @@ -276,9 +280,20 @@ export const Notification = ({ event }) => { - - - + + + {event.user_id && + + + + } + {event.system_id && + + + + } + + diff --git a/ui/pages/_app.js b/ui/pages/_app.js index 2940cb5b1ba..fd59dfab304 100644 --- a/ui/pages/_app.js +++ b/ui/pages/_app.js @@ -85,6 +85,7 @@ const EventsSubsciptionProvider = () => { rtkStore.dispatch(pushEvent({ ...result.event, user_id : result.event.userID, + system_id : result.event.systemID, updated_at : result.event.updatedAt, created_at : result.event.createdAt, deleted_at : result.event.deletedAt, From 745b0a63467731f6e34f9bbd208190c553c55b1a Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 16 Sep 2023 00:59:02 +0530 Subject: [PATCH 18/44] Extracted the TypingFilter Component Signed-off-by: aabidsofi19 --- .../NotificationCenter/NotificationOld.js | 329 ----------- ui/components/NotificationCenter/filter.js | 556 ++---------------- ui/components/TypingFilter/README.md | 130 ++++ ui/components/TypingFilter/index.js | 296 ++++++++++ ui/components/TypingFilter/state.js | 154 +++++ ui/components/TypingFilter/style.js | 54 ++ ui/components/TypingFilter/utils.js | 56 ++ ui/rtk-query/notificationCenter.js | 71 +-- 8 files changed, 761 insertions(+), 885 deletions(-) delete mode 100644 ui/components/NotificationCenter/NotificationOld.js create mode 100644 ui/components/TypingFilter/README.md create mode 100644 ui/components/TypingFilter/index.js create mode 100644 ui/components/TypingFilter/state.js create mode 100644 ui/components/TypingFilter/style.js create mode 100644 ui/components/TypingFilter/utils.js diff --git a/ui/components/NotificationCenter/NotificationOld.js b/ui/components/NotificationCenter/NotificationOld.js deleted file mode 100644 index 9470825def3..00000000000 --- a/ui/components/NotificationCenter/NotificationOld.js +++ /dev/null @@ -1,329 +0,0 @@ -import React, { useEffect, useState } from "react"; -import classNames from "classnames"; -import CheckCircleIcon from "@material-ui/icons/CheckCircle"; -import ErrorIcon from "@material-ui/icons/Error"; -import InfoIcon from "@material-ui/icons/Info"; -import DoneIcon from '@material-ui/icons/Done'; -import DeleteIcon from "@material-ui/icons/Delete" -import IconButton from "@material-ui/core/IconButton"; -import Grid from '@material-ui/core/Grid'; -import { SnackbarContent } from 'notistack'; -import WarningIcon from "@material-ui/icons/Warning"; -import { withStyles } from "@material-ui/core/styles"; -import Collapse from '@material-ui/core/Collapse'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import Card from '@material-ui/core/Card'; -import CardActions from '@material-ui/core/CardActions'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { EVENT_TYPES , NOTIFICATION_STATUS } from "../../lib/event-types"; -import ReplyIcon from '@material-ui/icons/Reply'; -import { - TwitterShareButton, - LinkedinShareButton, - FacebookShareButton, - TwitterIcon, - LinkedinIcon, - FacebookIcon -} from "react-share" -import { ClickAwayListener, Fade, Popper } from "@material-ui/core"; -import moment from "moment"; - -const variantIcon = { - [EVENT_TYPES.SUCCESS.type] : CheckCircleIcon, - [EVENT_TYPES.WARNING.type] : WarningIcon, - [EVENT_TYPES.ERROR.type] : ErrorIcon, - [EVENT_TYPES.DEFAULT.type] : InfoIcon, - [EVENT_TYPES.INFO.type] : InfoIcon, -}; - -const variantHoverColor = { - [EVENT_TYPES.SUCCESS.type] : "iconSuccess", - [EVENT_TYPES.WARNING.type] : "iconWarning", - [EVENT_TYPES.ERROR.type] : "iconError", - [EVENT_TYPES.INFO.type] : "iconInfo", - [EVENT_TYPES.DEFAULT.type] : "iconInfo" -} - -const styles = (theme) => ({ - [EVENT_TYPES.SUCCESS.type] : { color : "#6fbf73", }, - [EVENT_TYPES.ERROR.type] : { color : "#ff1744", }, - [EVENT_TYPES.INFO.type] : { color : "#2196f3", }, - [EVENT_TYPES.WARNING.type] : { color : "#ffc400", }, - [EVENT_TYPES.DEFAULT.type] : { color : "#edeff1", }, - iconColor : { color : "rgba(102, 102, 102, 1)" }, - iconSuccess : { "&:hover" : { color : "#6fbf73" } }, - iconError : { "&:hover" : { color : "#ff1744" } }, - iconInfo : { "&:hover" : { color : "#2196f3" } }, - iconWarning : { "&:hover" : { color : "#ffc400" } }, - icon : { fontSize : 20, }, - iconVariant : { - opacity : 0.9, - marginRight : theme.spacing(1), - }, - message : { - display : "flex", - alignItems : "center", - }, - timestamp : { - color : "#ebeff1", - fontSize : "0.8rem", - fontStyle : "italic", - }, - snackbarContent : { [theme.breakpoints.up("sm")] : { minWidth : "344px !important", }, }, - snackbarContentBorder : { - border : "1px solid rgba(102, 102, 102, 1)" - }, - card : { - backgroundColor : "rgba(50, 50, 50)", - width : "100%", - }, - actionRoot : { padding : "8px 8px 8px 16px", }, - icons : { marginLeft : "auto", }, - expand : { - padding : "8px 8px", - transform : "rotate(0deg)", - transition : theme.transitions.create("transform", { duration : theme.transitions.duration.shortest, }), - }, - expandOpen : { transform : "rotate(180deg)", }, - collapse : { padding : 16, }, - checkIcon : { - fontSize : 20, - color : "#b3b3b3", - paddingRight : 4, - }, - button : { - padding : 0, - textTransform : "none", - }, - share : { - transform : "scaleX(-1)" - }, - popper : { - width : 500, - }, - paper : { - padding : theme.spacing(1) - }, - shareIcon : { - margin : theme.spacing(0.4) - } -}); - -const generateMsgForMesh = (name) => { - return `I deployed ${name} service mesh with one-click using @mesheryio!\nManage your infrastructure with Meshery` -} - -const generateMsgForSampleApp = (name) => { - return `I deployed ${name} with one-click using @mesheryio!\nManage your infrastructure with Meshery` -} - -const generateMsgForAppsPatt = (name) => { - return `I deployed ${name} [design | application] in a single-click using @mesheryio!\nFind design patterns like mine in the Meshery Catalog - https://meshery.io/catalog` -} - -const getDefaultMessage = (message) => { - const msg = `" ${message} " - Manage your infrastructure with Meshery - ` - return msg -} - -const formatTimestamp = (utcTimestamp ) => { - const currentUtcTimestamp = moment.utc().valueOf() - - const timediff = currentUtcTimestamp - utcTimestamp - if (timediff >= 24 * 60 *60 *1000) { - return moment(utcTimestamp).local().format('YYYY-MM-DD HH:mm') - } - return moment(utcTimestamp).fromNow() -} - - -function Notification(props) { - const { - classes, className,expand,onMarkAsRead,onDeleteEvent - } = props; - - const { - summary,event_type, - details, probable_cause, - suggested_remediation, error_code, - component_type, component_name, - timestamp,status } = props.event - - const variant = event_type.type - const Icon = variantIcon[variant]; - const ERROR_DOC_LINK = "https://docs.meshery.io/reference/error-codes" - const [expanded, setExpanded] = useState(false); - const [cardHover, setCardHover] = useState(false) - const [socialExpand, setSocialExpand] = useState(false); - const [anchorEl, setAnchorEl] = useState(null); - const [socialMessage, setSocialMessage] = useState(""); - const [highlight, setHighlight] = useState(false) - - const handleClose = () => { - if (status == NOTIFICATION_STATUS.VIEWED) { - onDeleteEvent() - } else { - onMarkAsRead() - } - } - - const handleExpandClick = () => { - setExpanded(!expanded); - }; - - const handleSocialExpandClick = (e) => { - setAnchorEl(e.currentTarget); - e.stopPropagation(); - setSocialExpand(socialExpand => !socialExpand); - } - useEffect(() => { - if (expand && !expanded) { - handleExpandClick(); - setHighlight(true) - setTimeout(() => { - setHighlight(false) - }, 3000) - } - if (component_type === "adapter") { - if (summary?.includes("mesh installed")) { - setSocialMessage(generateMsgForMesh(component_name[0].toUpperCase() + component_name.substring(1).toLowerCase())) - return - } - if (summary.includes("application installed")) { - const name = summary?.split(" ")[0]; - setSocialMessage(generateMsgForSampleApp(name[0].toUpperCase() + name.substring(1).toLowerCase())) - return - } - setSocialMessage(getDefaultMessage(summary)) - return - } - if (component_type === "core" && summary?.includes("deployed")) { - const designName = summary?.split(":")[1] - setSocialMessage(generateMsgForAppsPatt(designName)) - } - - },[expand]) - - - return ( - - - - - -
      - -
      - {summary} - {formatTimestamp(timestamp)} -
      -
      -
      - - - setCardHover(true)} - onMouseLeave={() => setCardHover(false)} - > - - - - {variant === EVENT_TYPES.SUCCESS.type && - handleSocialExpandClick(e)} - > - setCardHover(false)} - onMouseLeave={() => setCardHover(true)} - /> - - } - - {status == NOTIFICATION_STATUS.VIEWED && - - setCardHover(false)} - onMouseLeave={() => setCardHover(true)} - /> - } - {status == NOTIFICATION_STATUS.NEW && - - setCardHover(false)} - onMouseLeave={() => setCardHover(true)} - /> - } - -
      -
      - - - DETAILS - {details} - - {variant === EVENT_TYPES.ERROR.type && - <> - - { probable_cause && - - PROBABLE CAUSE - {probable_cause} - - } - { suggested_remediation && - - SUGGESTED REMEDIATION - {suggested_remediation} - - } - {component_name && - - ERROR CODE - {error_code} - } - - } - -
      - - {({ TransitionProps }) => ( - setSocialExpand(false)}> - - - - - - - - - - - - - - - )} - -
      -
      -
      - ); -} - -export default withStyles(styles)(Notification); \ No newline at end of file diff --git a/ui/components/NotificationCenter/filter.js b/ui/components/NotificationCenter/filter.js index 45ccee0009e..6ab39deeb6e 100644 --- a/ui/components/NotificationCenter/filter.js +++ b/ui/components/NotificationCenter/filter.js @@ -1,538 +1,48 @@ -import { - ClickAwayListener, - Divider, - Fade, - IconButton, - InputAdornment, - List, - Popper, - TextField, - Typography, - alpha, - makeStyles, - useTheme, -} from "@material-ui/core"; -import ContentFilterIcon from "../../assets/icons/ContentFilterIcon"; -import { useEffect, useReducer, useRef, useState } from "react"; -import CrossCircleIcon from "../../assets/icons/CrossCircleIcon"; -import clsx from "clsx"; +import { useGetEventFiltersQuery } from "../../rtk-query/notificationCenter"; +import TypingFilter from "../TypingFilter"; import { SEVERITY, STATUS } from "./constants"; -const useStyles = makeStyles((theme) => ({ - root : { - position : "relative", - backgroundColor : theme.palette.secondary.elevatedComponents, - }, - input : { - width : "100%", - "& .MuiOutlinedInput-root" : { - borderRadius : "6px", - backgroundColor : theme.palette.secondary.searchBackground, - "& fieldset" : { - borderRadius : "6px", - border : `2px solid ${theme.palette.secondary.searchBorder}`, - }, - }, - }, - - dropDown : { - backgroundColor : theme.palette.secondary.searchBackground, - borderRadius : "6px", - boxShadow : - "0px 2px 4px 0px rgba(0, 0, 0, 0.20), 0px 1px 10px 0px rgba(0, 0, 0, 0.12), 0px 4px 5px 0px rgba(0, 0, 0, 0.14)", - border : `2px solid ${theme.palette.secondary.searchBorder}`, - marginTop : "0.2rem", - }, -})); - -const useFilterStyles = makeStyles((theme) => ({ - item : { - fontFamily : "Qanelas Soft, sans-serif", - display : "flex", - gap : "0.3rem", - margin : "0.3rem", - padding : "0.3rem", - paddingInline : "1rem", - borderRadius : "6px", - cursor : "pointer", - "&:hover" : { - backgroundColor : alpha(theme.palette.secondary.link2, 0.25), - }, - }, - - label : { - fontWeight : 500, - color : theme.palette.secondary.icon, - }, - description : { - fontWeight : 400, - color : theme.palette.secondary.number, - }, -})); - -const FILTERS = { - SEVERITY : { - value : "severity", - label : "Severity", - description : "Filter by severity", - values : Object.values(SEVERITY), - }, - - STATUS : { - value : "status", - label : "Status", - description : "Filter by status", - values : Object.values(STATUS), - type : "string" - }, - - TYPE : { - value : "type", - label : "Type", - description : "Filter by type", - }, - - AUTHOR : { - value : "author", - label : "Author", - description : "Filter by any user or system", - }, - - CATEGORY : { - value : "category", - label : "Category", - description : "Filter by category", - values : ["pattern", "connection"], - }, -}; - -const FILTERING_STATE = { - IDLE : "idle", - SELECTING_FILTER : "selecting_filter", - SELECTING_VALUE : "selecting_value", -}; - -const FILTER_EVENTS = { - START : "start", - SELECT : "select_filter", - SELECT_FILTER : "select_filter", - INPUT_CHANGE : "input_change", - SELECT_FILTER_VALUE : "select_filter_value", - CLEAR : "clear", - EXIT : "exit", -}; - -const Delimiter = { - FILTER : " ", - FILTER_VALUE : ":", -}; - -/** - * Parses a filter string and returns a filter object. - * - * @param {string} filterString - The input filter string of the form "type:value type2:value2 type:value2". - * @returns {Object} - The filter object with types as keys and arrays of values as values. - */ -const getFilters = (filterString) => { - const filters = {}; - const filterValuePairs = filterString.split(Delimiter.FILTER); - filterValuePairs.forEach((filterValuePair) => { - const [filter, value] = filterValuePair.split(Delimiter.FILTER_VALUE); - - if (filter == FILTERS.STATUS.value) { - filters[filter] = value; - return - } - - if (filter && value) { - filters[filter] = filters[filter] || []; - if (!filters[filter].includes(value)) { - filters[filter].push(value) - } - } - }); - - - return filters; -}; - -// return a filter string of form "type:value type2:value2 type:value2" -// from a filter object of form { type : {values} , type2 : {values} } -export const getFilterString = (filters) => { - return Object.entries(filters).reduce((filterString, [filter, values]) => { - return filterString + [...values].map((value) => `${filter}${Delimiter.FILTER_VALUE}${value}`).join(" "); - }, ""); -}; - - - -const commonReducer = (stateMachine, action) => { - const { context } = stateMachine; - switch (action.type) { - case FILTER_EVENTS.CLEAR: - return { - state : FILTERING_STATE.SELECTING_FILTER, - context : { - ...context, - value : "", - prevValue : [""], - }, - }; - - case FILTER_EVENTS.EXIT: - return { - state : FILTERING_STATE.IDLE, - context : { - ...context, - value : "", - prevValue : [""], - }, - }; - - default: - return stateMachine; - } -}; - -const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { - const { state, context } = stateMachine; - const nextDelimiter = nextState == FILTERING_STATE.SELECTING_FILTER ? Delimiter.FILTER : Delimiter.FILTER_VALUE; - const prevDelimiter = nextDelimiter == Delimiter.FILTER_VALUE ? Delimiter.FILTER : Delimiter.FILTER_VALUE; - const prevState = nextState; // same beccuase the prevState is the same as the nextState ( as we have only two states) - switch (action.type) { - // Select a filter and move to start entring its value - case FILTER_EVENTS.SELECT: { - const newValue = nextValue(context.prevValue.at(-1), action.payload.value); // ":" is used to separate the filter and its value) - return { - state : nextState, - context : { - ...context, - value : newValue + nextDelimiter, - prevValue : [...context.prevValue, newValue], - }, - }; - } - //" " is used to separate multiple filters - case FILTER_EVENTS.INPUT_CHANGE: - // prevent transition when the the filter/value is empty - if (action.payload.value.endsWith(nextDelimiter) && context.value.endsWith(prevDelimiter)) { - return stateMachine; - } - - // prevent adding multiple delimeters together - if (action.payload.value.endsWith(prevDelimiter) && context.value.endsWith(prevDelimiter)) { - return stateMachine; - } - - if (action.payload.value == context.prevValue.at(-1)) { - return { - state : prevState, - context : { - ...context, - prevValue : context.prevValue.slice(0, -1), - value : action.payload.value, - }, - }; - } - - if (action.payload.value.endsWith(nextDelimiter)) { - const newValue = action.payload.value; - return { - state : nextState, - context : { - ...context, - value : action.payload.value, - prevValue : [...context.prevValue, newValue.slice(0, -1)], - }, - }; - } - - return { - state, // stay in the same state - context : { - ...context, - value : action.payload.value, - }, - }; - default: - return commonReducer(stateMachine, action); - } -}; - -const filterReducer = (stateMachine, action) => { - const { state } = stateMachine; - switch (state) { - // Initial State - case FILTERING_STATE.IDLE: - switch (action.type) { - // Start the filter process - case "START": - return { - ...stateMachine, - state : FILTERING_STATE.SELECTING_FILTER, - }; - default: - return stateMachine; - } - - case FILTERING_STATE.SELECTING_FILTER: - // return filterSelectionReducer(stateMachine, action); - return filterSelectionReducer( - stateMachine, - action, - FILTERING_STATE.SELECTING_VALUE, - (prevValue, value) => prevValue + Delimiter.FILTER + value - ); - - case FILTERING_STATE.SELECTING_VALUE: - return filterSelectionReducer( - stateMachine, - action, - FILTERING_STATE.SELECTING_FILTER, - (prevValue, value) => prevValue + Delimiter.FILTER_VALUE + value - ); - - // runs for all states - default: - return stateMachine; - } -}; - -const getCurrentFilterAndValue = (filteringState) => { - const { context } = filteringState; - const currentFilterValue = context.value.split(Delimiter.FILTER).at(-1); - const currentFilter = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[0] || ""; - const currentValue = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[1] || ""; - return { - filter : currentFilter, - value : currentValue, - }; -}; -const Filters = ({ filterStateMachine, dispatchFilterMachine }) => { - const classes = useFilterStyles(); - const selectFilter = (filter) => { - dispatchFilterMachine({ - type : FILTER_EVENTS.SELECT, - payload : { - value : filter, - }, - }); - }; - - const { filter : currentFilter } = getCurrentFilterAndValue(filterStateMachine); - const matchingFilters = currentFilter - ? Object.values(FILTERS).filter((filter) => filter.value.startsWith(currentFilter)) - : Object.values(FILTERS); - return ( - - {matchingFilters.length == 0 && ( -
      - - Sorry we dont currently support this filter - -
      - )} - {matchingFilters.map((filter) => { - return ( - <> -
      selectFilter(filter.value)}> - - {filter.value}: - - - {filter.description} - -
      - - - ); - })} -
      - ); -}; +const useFilterSchema = () => { -const FilterValueSuggestions = ({ filterStateMachine, dispatchFilterMachine }) => { - const classes = useFilterStyles(); - - const selectValue = (value) => { - dispatchFilterMachine({ - type : FILTER_EVENTS.SELECT, - payload : { - value, - }, - }); - }; - const { filter, value } = getCurrentFilterAndValue(filterStateMachine); - const currentFilter = Object.values(FILTERS).find((f) => f.value == filter); - const suggestions = currentFilter?.values?.filter((v) => v.startsWith(value)) || []; + const { data } = useGetEventFiltersQuery(); - return ( - - {suggestions.length == 0 && ( -
      - - No results available - -
      - )} - {suggestions.map((value) => { - return ( - <> -
      selectValue(value)}> - - {value} - -
      - - - ); - })} -
      - ); -}; - -const Filter = ({ handleFilter }) => { - const theme = useTheme(); - // console.log("initialFilter", initialFilter) - const classes = useStyles(); - const [anchorEl, setAnchorEl] = useState(null); - const isPopperOpen = Boolean(anchorEl); - const inputFieldRef = useRef(null); - const [filteringState, dispatch] = useReducer(filterReducer, { - context : { - value : "", - prevValue : [""], + return { + SEVERITY : { + value : "severity", + description : "Filter by severity", + values : Object.values(SEVERITY), }, - state : FILTERING_STATE.IDLE, - }); - - const handleFilterChange = (e) => { - if (!anchorEl) { - setAnchorEl(e.currentTarget) - } - - if (e.target.value === "") { - return dispatch({ - type : FILTER_EVENTS.CLEAR, - }); - } - - return dispatch({ - type : FILTER_EVENTS.INPUT_CHANGE, - payload : { - value : e.target.value, - }, - }); - }; - - const handleClear = () => { - dispatch({ - type : FILTER_EVENTS.EXIT, - }); - - handleFilter({}) - }; + STATUS : { + value : "status", + description : "Filter by status", + values : Object.values(STATUS), + multiple : false, + }, - const handleFocus = (e) => { - setAnchorEl(e.currentTarget); - dispatch({ type : "START" }); - }; + ACTION : { + value : "action", + values : data?.action || [], + description : "Filter by type", + }, - const handleClickAway = (e) => { - if (inputFieldRef.current.contains(e.target)) { - return; - } + AUTHOR : { + value : "author", + description : "Filter by any user or system", + }, - setAnchorEl(null); + CATEGORY : { + value : "category", + description : "Filter by category", + values : data?.category || [], + }, }; +} - //add enter event listener to the input fieldse - //add esc event listener to the input fields - useEffect(() => { - - if (!inputFieldRef.current) { - return; - } - - const handleKeyDown = (e) => { - if (e.key == "Enter") { - handleFilter(getFilters(e.target.value)) - setAnchorEl(null); - } - } - inputFieldRef?.current?.addEventListener("keydown", handleKeyDown) - return () => { - inputFieldRef?.current?.removeEventListener("keydown", handleKeyDown) - } - }, [inputFieldRef.current]) - - - - return ( -
      - - {" "} - {" "} - - ), - endAdornment : ( - - - {filteringState.state !== FILTERING_STATE.IDLE && ( - - )} - - - ), - }} - /> - - - {({ TransitionProps }) => { - return ( - - -
      - {filteringState.state == FILTERING_STATE.SELECTING_FILTER && ( - - )} - {filteringState.state == FILTERING_STATE.SELECTING_VALUE && ( - - )} -
      -
      -
      - ); - }} -
      -
      - ); +const Filter = ({ handleFilter }) => { + const filterSchema = useFilterSchema(); + return ; }; export default Filter; \ No newline at end of file diff --git a/ui/components/TypingFilter/README.md b/ui/components/TypingFilter/README.md new file mode 100644 index 00000000000..d241f76d840 --- /dev/null +++ b/ui/components/TypingFilter/README.md @@ -0,0 +1,130 @@ +# TypingFilter Component + +The `TypingFilter` component is a customizable React component that enables real-time filtering and selection based on user input. +It provides a user-friendly interface for filtering data in your application. + +## Table of Contents + +- [Usage](#usage) +- [Props](#props) +- [Examples](#examples) + +## Usage + +The `TypingFilter` component is designed to provide an interactive filtering experience in your application. Here's how you can use it: + +```javascript +import React from 'react'; +import TypingFilter from './path-to-TypingFilter'; + +function MyComponent() { + // Define a filter schema that describes the available filter options. + const filterSchema = { + // Define your filter categories here + // Example: + SEVERITY: { + value: "severity", + description: "Filter by severity", + values: ["Low", "Medium", "High"], + multiple : true // default + }, + // Add more filter categories as needed + }; + + // Define a callback function to handle filter changes. + const handleFilterChange = (filteredData) => { + // Implement your logic to react to the filtered data. + // This function will be called when the user applies a filter. ( on presing enter in input) + console.log("Filtered data:", filteredData); + }; + + return ( +
      + + {/* Your other components */} +
      + ); +} + +export default MyComponent; +``` + +## Props + +The `TypingFilter` component accepts the following props: + +- `filterSchema` (object, required): An object that defines available filter options. Each property of this object represents a filter category with the following properties: + - `value` (string, required): The filter name used for filtering within the category. + - `description` (string, required): Description of the filter category. + - `type` (string, optional): The data type of the filter (e.g., "string", "number"). + - `values` (array, optional): Possible values for the filter. + +- `handleFilter` (function, required): A callback function that is called when the user applies a filter. This function receives the filtered data as an argument. + + +# Finite State Machine (FSM) for `TypingFilter` Component + +This README provides an overview of the Finite State Machine (FSM) implementation used to manage the state of the `TypingFilter` component. +The FSM is responsible for handling user interactions, such as selecting filters, entering values, and clearing the filter, within the component. + +## Table of Contents + +- [Overview](#overview) +- [State Definitions](#state-definitions) +- [Reducers](#reducers) +- [State Transitions](#state-transitions) +- [Initial State Handling](#initial-state-handling) + +## Overview + +The FSM implementation within the `TypingFilter` component ensures that user interactions are correctly processed and managed, resulting in a smooth and intuitive filtering experience. + +## State Definitions + +The FSM code defines three sets of constants to represent important elements within the state management: + +### 1. `FILTERING_STATE` + +Defines the possible states that the `TypingFilter` component can be in. These states include: +- `IDLE`: Represents the initial state when the component is not actively filtering. +- `SELECTING_FILTER`: Indicates that the user is selecting a filter. +- `SELECTING_VALUE`: Indicates that the user is entering a filter value. + +### 2. `FILTER_EVENTS` + +Represents the events that trigger state transitions within the FSM. Some of the events include: +- `START`: Initiates the filtering process. +- `SELECT`: Indicates the selection of a filter. +- `INPUT_CHANGE`: Represents a change in the filter input. +- `CLEAR`: Clears the filter. +- `EXIT`: Exits the filtering process. + +### 3. `Delimiter` + +Defines delimiters used to separate filter and value entries within the component. Delimiters include: +- `FILTER`: Separates multiple filters. +- `FILTER_VALUE`: Separates filters from their corresponding values. + +## Reducers + +The FSM implementation includes two key reducer functions: + +### 1. `commonReducer` + +This common reducer function handles events that are common across all states. It includes logic to handle "CLEAR" and "EXIT" events, which reset the component's state and clear any entered values. + +### 2. `filterSelectionReducer` + +The `filterSelectionReducer` is a specific reducer used to manage transitions between "SELECTING_FILTER" and "SELECTING_VALUE" states. It handles events related to selecting filters and entering values. The logic ensures that delimiters are appropriately added or removed when the user interacts with the filter. + +## State Transitions + +State transitions are managed based on user actions and the current state of the component. For example, when the user selects a filter, the state transitions from "SELECTING_FILTER" to "SELECTING_VALUE." When the user inputs values or clears the filter, the state transitions are managed accordingly. + +## Initial State Handling + +The FSM implementation includes handling for the initial state, where it listens for the "START" event to transition from "IDLE" to "SELECTING_FILTER." This ensures that the filtering process is initiated when the user interacts with the component. + diff --git a/ui/components/TypingFilter/index.js b/ui/components/TypingFilter/index.js new file mode 100644 index 00000000000..7aeece4c3e5 --- /dev/null +++ b/ui/components/TypingFilter/index.js @@ -0,0 +1,296 @@ +import { + ClickAwayListener, + Divider, + Fade, + IconButton, + InputAdornment, + List, + Popper, + TextField, + Typography, + useTheme, +} from "@material-ui/core"; +import ContentFilterIcon from "../../assets/icons/ContentFilterIcon"; +import { useEffect, useReducer, useRef, useState } from "react"; +import CrossCircleIcon from "../../assets/icons/CrossCircleIcon"; +import clsx from "clsx"; +import { useStyles, useFilterStyles } from "./style"; +import { FILTERING_STATE, FILTER_EVENTS, filterReducer } from "./state"; +import { getFilters,getCurrentFilterAndValue } from "./utils" + +const Filters = ({ filterStateMachine, dispatchFilterMachine, filterSchema }) => { + const classes = useFilterStyles(); + const selectFilter = (filter) => { + dispatchFilterMachine({ + type : FILTER_EVENTS.SELECT, + payload : { + value : filter, + }, + }); + }; + + const { filter : currentFilter } = getCurrentFilterAndValue(filterStateMachine); + const matchingFilters = currentFilter + ? Object.values(filterSchema).filter((filter) => filter.value.startsWith(currentFilter)) + : Object.values(filterSchema); + return ( + + {matchingFilters.length == 0 && ( +
      + + Sorry we dont currently support this filter + +
      + )} + {matchingFilters.map((filter) => { + return ( + <> +
      selectFilter(filter.value)}> + + {filter.value}: + + + {filter.description} + +
      + + + ); + })} +
      + ); +}; + +const FilterValueSuggestions = ({ filterStateMachine, dispatchFilterMachine, filterSchema }) => { + const classes = useFilterStyles(); + + const selectValue = (value) => { + dispatchFilterMachine({ + type : FILTER_EVENTS.SELECT, + payload : { + value, + }, + }); + }; + const { filter, value } = getCurrentFilterAndValue(filterStateMachine); + const currentFilter = Object.values(filterSchema).find((f) => f.value == filter); + const suggestions = currentFilter?.values?.filter((v) => v.startsWith(value)) || []; + + return ( + + {suggestions.length == 0 && ( +
      + + No results available + +
      + )} + {suggestions.map((value) => { + return ( + <> +
      selectValue(value)}> + + {value} + +
      + + + ); + })} +
      + ); +}; + + +/** + * Filter Schema Object + * + * The `filterSchema` object defines available filter options for the TypingFilter component. + * It provides information about different filter categories, their descriptions, and possible values. + * + * @typedef {object} FilterSchema + * @property {object} [CATEGORY_NAME] - An object representing a filter category. + * @property {string} [CATEGORY_NAME.value] - The filter name used for filtering within the category. + * @property {string} [CATEGORY_NAME.description] - Description of the filter category. + * @property {string} [CATEGORY_NAME.type] - The data type of the filter (optional). + * @property {string[]} [CATEGORY_NAME.values] - Possible values for the filter (optional). + * + * @example + * // Example filter schema with multiple filter categories + * const filterSchema = { + * SEVERITY: { + * value: "severity", + * description: "Filter by severity", + * values: ["Low", "Medium", "High"], + * }, + * STATUS: { + * value: "status", + * description: "Filter by status", + * type: "string", + * values: ["Open", "Closed", "In Progress"], + * }, + * CUSTOM_FILTER: { + * value: "custom", + * description: "Custom filter description", + * type: "number", + * }, + * // Add more filter categories as needed + * }; + */ + +/** + * TypingFilter Component + * + * A component for real-time filtering and selection with typing. It provides a user-friendly + * interface for filtering data based on user input. + * + * @component + * @param {object} props - Component props. + * @param {FilterSchema} filterSchema - The schema defining available filter options. + * @param {function} handleFilter - A callback function to handle filter changes. + * @returns {JSX.Element} - A React JSX element representing the TypingFilter component. + */ +const TypingFilter = ({ filterSchema, handleFilter }) => { + const theme = useTheme(); + // console.log("initialFilter", initialFilter) + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(null); + const isPopperOpen = Boolean(anchorEl); + const inputFieldRef = useRef(null); + const [filteringState, dispatch] = useReducer(filterReducer, { + context : { + value : "", + prevValue : [""], + }, + state : FILTERING_STATE.IDLE, + }); + + const handleFilterChange = (e) => { + + if (!anchorEl) { + setAnchorEl(e.currentTarget) + } + + if (e.target.value === "") { + return dispatch({ + type : FILTER_EVENTS.CLEAR, + }); + } + + return dispatch({ + type : FILTER_EVENTS.INPUT_CHANGE, + payload : { + value : e.target.value, + }, + }); + }; + + const handleClear = () => { + dispatch({ + type : FILTER_EVENTS.EXIT, + }); + + handleFilter({}) + }; + + const handleFocus = (e) => { + setAnchorEl(e.currentTarget); + dispatch({ type : "START" }); + }; + + const handleClickAway = (e) => { + if (inputFieldRef.current.contains(e.target)) { + return; + } + + setAnchorEl(null); + }; + + //add enter event listener to the input fieldse + //add esc event listener to the input fields + useEffect(() => { + + if (!inputFieldRef.current) { + return; + } + + const handleKeyDown = (e) => { + if (e.key == "Enter") { + handleFilter(getFilters(e.target.value, filterSchema)) + setAnchorEl(null); + } + } + inputFieldRef?.current?.addEventListener("keydown", handleKeyDown) + return () => { + inputFieldRef?.current?.removeEventListener("keydown", handleKeyDown) + } + }, [inputFieldRef.current]) + + + + return ( +
      + + {" "} + {" "} + + ), + endAdornment : ( + + + {filteringState.state !== FILTERING_STATE.IDLE && ( + + )} + + + ), + }} + /> + + + {({ TransitionProps }) => { + return ( + + +
      + {filteringState.state == FILTERING_STATE.SELECTING_FILTER && ( + + )} + {filteringState.state == FILTERING_STATE.SELECTING_VALUE && ( + + )} +
      +
      +
      + ); + }} +
      +
      + ); +}; + +export default TypingFilter; \ No newline at end of file diff --git a/ui/components/TypingFilter/state.js b/ui/components/TypingFilter/state.js new file mode 100644 index 00000000000..84b89fb98c9 --- /dev/null +++ b/ui/components/TypingFilter/state.js @@ -0,0 +1,154 @@ +export const FILTERING_STATE = { + IDLE : "idle", + SELECTING_FILTER : "selecting_filter", + SELECTING_VALUE : "selecting_value", +}; + +export const FILTER_EVENTS = { + START : "start", + SELECT : "select_filter", + SELECT_FILTER : "select_filter", + INPUT_CHANGE : "input_change", + SELECT_FILTER_VALUE : "select_filter_value", + CLEAR : "clear", + EXIT : "exit", +}; + +export const Delimiter = { + FILTER : " ", + FILTER_VALUE : ":", +}; + + +const commonReducer = (stateMachine, action) => { + const { context } = stateMachine; + switch (action.type) { + case FILTER_EVENTS.CLEAR: + return { + state : FILTERING_STATE.SELECTING_FILTER, + context : { + ...context, + value : "", + prevValue : [""], + }, + }; + + case FILTER_EVENTS.EXIT: + return { + state : FILTERING_STATE.IDLE, + context : { + ...context, + value : "", + prevValue : [""], + }, + }; + + default: + return stateMachine; + } +}; + +const filterSelectionReducer = (stateMachine, action, nextState, nextValue) => { + const { state, context } = stateMachine; + const nextDelimiter = nextState == FILTERING_STATE.SELECTING_FILTER ? Delimiter.FILTER : Delimiter.FILTER_VALUE; + const prevDelimiter = nextDelimiter == Delimiter.FILTER_VALUE ? Delimiter.FILTER : Delimiter.FILTER_VALUE; + const prevState = nextState; // same beccuase the prevState is the same as the nextState ( as we have only two states) + switch (action.type) { + // Select a filter and move to start entring its value + case FILTER_EVENTS.SELECT: { + const newValue = nextValue(context.prevValue.at(-1), action.payload.value); // ":" is used to separate the filter and its value) + return { + state : nextState, + context : { + ...context, + value : newValue + nextDelimiter, + prevValue : [...context.prevValue, newValue], + }, + }; + } + //" " is used to separate multiple filters + case FILTER_EVENTS.INPUT_CHANGE: + // prevent transition when the the filter/value is empty + if (action.payload.value.endsWith(nextDelimiter) && context.value.endsWith(prevDelimiter)) { + return stateMachine; + } + + // prevent adding multiple delimeters together + if (action.payload.value.endsWith(prevDelimiter) && context.value.endsWith(prevDelimiter)) { + return stateMachine; + } + + if (action.payload.value == context.prevValue.at(-1)) { + return { + state : prevState, + context : { + ...context, + prevValue : context.prevValue.slice(0, -1), + value : action.payload.value, + }, + }; + } + + if (action.payload.value.endsWith(nextDelimiter)) { + const newValue = action.payload.value; + return { + state : nextState, + context : { + ...context, + value : action.payload.value, + prevValue : [...context.prevValue, newValue.slice(0, -1)], + }, + }; + } + + return { + state, // stay in the same state + context : { + ...context, + value : action.payload.value, + }, + }; + default: + return commonReducer(stateMachine, action); + } +}; + +export const filterReducer = (stateMachine, action) => { + const { state } = stateMachine; + switch (state) { + // Initial State + case FILTERING_STATE.IDLE: + switch (action.type) { + // Start the filter process + case "START": + return { + ...stateMachine, + state : FILTERING_STATE.SELECTING_FILTER, + }; + default: + return stateMachine; + } + + case FILTERING_STATE.SELECTING_FILTER: + // return filterSelectionReducer(stateMachine, action); + return filterSelectionReducer( + stateMachine, + action, + FILTERING_STATE.SELECTING_VALUE, + (prevValue, value) => prevValue + Delimiter.FILTER + value + ); + + case FILTERING_STATE.SELECTING_VALUE: + return filterSelectionReducer( + stateMachine, + action, + FILTERING_STATE.SELECTING_FILTER, + (prevValue, value) => prevValue + Delimiter.FILTER_VALUE + value + ); + + // runs for all states + default: + return stateMachine; + } +}; + diff --git a/ui/components/TypingFilter/style.js b/ui/components/TypingFilter/style.js new file mode 100644 index 00000000000..2bc6d6b40c7 --- /dev/null +++ b/ui/components/TypingFilter/style.js @@ -0,0 +1,54 @@ +import { makeStyles,alpha } from "@material-ui/core"; + +export const useStyles = makeStyles((theme) => ({ + root : { + position : "relative", + backgroundColor : theme.palette.secondary.elevatedComponents, + }, + input : { + width : "100%", + "& .MuiOutlinedInput-root" : { + borderRadius : "6px", + backgroundColor : theme.palette.secondary.searchBackground, + "& fieldset" : { + borderRadius : "6px", + border : `2px solid ${theme.palette.secondary.searchBorder}`, + }, + }, + }, + + dropDown : { + backgroundColor : theme.palette.secondary.searchBackground, + borderRadius : "6px", + boxShadow : + "0px 2px 4px 0px rgba(0, 0, 0, 0.20), 0px 1px 10px 0px rgba(0, 0, 0, 0.12), 0px 4px 5px 0px rgba(0, 0, 0, 0.14)", + border : `2px solid ${theme.palette.secondary.searchBorder}`, + marginTop : "0.2rem", + }, +})); + +export const useFilterStyles = makeStyles((theme) => ({ + item : { + fontFamily : "Qanelas Soft, sans-serif", + display : "flex", + gap : "0.3rem", + margin : "0.3rem", + padding : "0.3rem", + paddingInline : "1rem", + borderRadius : "6px", + cursor : "pointer", + "&:hover" : { + backgroundColor : alpha(theme.palette.secondary.link2, 0.25), + }, + }, + + label : { + fontWeight : 500, + color : theme.palette.secondary.icon, + }, + description : { + fontWeight : 400, + color : theme.palette.secondary.number, + }, +})); + diff --git a/ui/components/TypingFilter/utils.js b/ui/components/TypingFilter/utils.js new file mode 100644 index 00000000000..9e7facafb13 --- /dev/null +++ b/ui/components/TypingFilter/utils.js @@ -0,0 +1,56 @@ +import { Delimiter } from "./state"; + + +// returns the filter object from the filterSchema +const getFilterByValue = (value,filterSchema) => { + return Object.values(filterSchema).find((filter) => filter.value == value); +}; + +/** + * Parses a filter string and returns a filter object. + * + * @param {string} filterString - The input filter string of the form "type:value type2:value2 type:value2". + * @returns {Object} - The filter object with types as keys and arrays of values as values. + */ +export const getFilters = (filterString,filterSchema) => { + const filters = {}; + const filterValuePairs = filterString.split(Delimiter.FILTER); + filterValuePairs.forEach((filterValuePair) => { + const [filter, value] = filterValuePair.split(Delimiter.FILTER_VALUE); + + if (getFilterByValue(filter,filterSchema)?.multiple == false) { + filters[filter] = value; + return + } + + if (filter && value) { + filters[filter] = filters[filter] || []; + if (!filters[filter].includes(value)) { + filters[filter].push(value) + } + } + }); + + + return filters; +}; + +// return a filter string of form "type:value type2:value2 type:value2" +// from a filter object of form { type : {values} , type2 : {values} } +export const getFilterString = (filters) => { + return Object.entries(filters).reduce((filterString, [filter, values]) => { + return filterString + [...values].map((value) => `${filter}${Delimiter.FILTER_VALUE}${value}`).join(" "); + }, ""); +}; + + +export const getCurrentFilterAndValue = (filteringState) => { + const { context } = filteringState; + const currentFilterValue = context.value.split(Delimiter.FILTER).at(-1); + const currentFilter = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[0] || ""; + const currentValue = currentFilterValue.split(Delimiter.FILTER_VALUE)?.[1] || ""; + return { + filter : currentFilter, + value : currentValue, + }; +}; diff --git a/ui/rtk-query/notificationCenter.js b/ui/rtk-query/notificationCenter.js index 15d89410b48..084089baba8 100644 --- a/ui/rtk-query/notificationCenter.js +++ b/ui/rtk-query/notificationCenter.js @@ -20,16 +20,16 @@ function parseFilters(filters) { }, {}); } export const PROVIDER_TAGS = { - EVENT: "event" + EVENT : "event" } export const notificationCenterApi = api .enhanceEndpoints({ - addTagTypes: Object.values(PROVIDER_TAGS), + addTagTypes : Object.values(PROVIDER_TAGS), }) .injectEndpoints({ - endpoints: (builder) => ({ - getEvents: builder.query({ - query: ({ + endpoints : (builder) => ({ + getEvents : builder.query({ + query : ({ page = 1, filters = {} }) => { @@ -37,54 +37,58 @@ export const notificationCenterApi = api const parsedFilters = parseFilters(filters); // console.log("parsedFilters", parsedFilters) return { - url: `v2/events`, - params: { + url : `v2/events`, + params : { ...parsedFilters, - page: page, - sort: "created_at", - order: "desc", - page_size: 15 + page : page, + sort : "created_at", + order : "desc", + page_size : 15 } } }, - providesTags: [PROVIDER_TAGS.EVENT], + providesTags : [PROVIDER_TAGS.EVENT], // keepUnusedDataFor : "0.001" }), - getEventsSummary: builder.query({ - query: () => { + getEventsSummary : builder.query({ + query : () => { return { - url: `v2/events?page=$1&page_size=1` + url : `v2/events?page=$1&page_size=1` } }, - transformResponse: (response) => { + transformResponse : (response) => { return { - count_by_severity_level: response.count_by_severity_level, - total_count: response.total_count + count_by_severity_level : response.count_by_severity_level, + total_count : response.total_count } }, - providesTags: [PROVIDER_TAGS.EVENT], + providesTags : [PROVIDER_TAGS.EVENT], }), - updateStatus: builder.mutation({ - query: ({ id, status }) => ({ - url: `events/status/${id}`, - method: 'POST', - body: { - status: status + updateStatus : builder.mutation({ + query : ({ id, status }) => ({ + url : `events/status/${id}`, + method : 'POST', + body : { + status : status } }), - invalidatesTags: [PROVIDER_TAGS.EVENT], + invalidatesTags : [PROVIDER_TAGS.EVENT], }), - deleteEvent: builder.mutation({ - query: ({ id }) => ({ - url: `events/${id}`, - method: 'DELETE', + deleteEvent : builder.mutation({ + query : ({ id }) => ({ + url : `events/${id}`, + method : 'DELETE', }), - invalidatesTags: [PROVIDER_TAGS.EVENT], + invalidatesTags : [PROVIDER_TAGS.EVENT], + }), + + getEventFilters : builder.query({ + query : () => `events/types` }) }), - overrideExisting: false, + overrideExisting : false, }) export const { @@ -92,4 +96,5 @@ export const { useUpdateStatusMutation, useDeleteEventMutation, useLazyGetEventsQuery, -} = notificationCenterApi + useGetEventFiltersQuery, +} = notificationCenterApi \ No newline at end of file From a4621a22609b2511699e7d3ddf58b91d5d173bce Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Fri, 15 Sep 2023 15:18:33 -0500 Subject: [PATCH 19/44] docs Signed-off-by: Lee Calcote --- .../contributing-ui-notification-cener.md | 179 ++++++++++++++++++ scripts/component_generation/dump.csv | 1 + 2 files changed, 180 insertions(+) create mode 100644 docs/pages/project/contributing/contributing-ui-notification-cener.md create mode 100644 scripts/component_generation/dump.csv diff --git a/docs/pages/project/contributing/contributing-ui-notification-cener.md b/docs/pages/project/contributing/contributing-ui-notification-cener.md new file mode 100644 index 00000000000..7146535a6a9 --- /dev/null +++ b/docs/pages/project/contributing/contributing-ui-notification-cener.md @@ -0,0 +1,179 @@ +--- +layout: page +title: Contributing to Meshery UI - Notification Center +permalink: project/contributing/contributing-ui-notification-center +description: How to contribute to the Notification Center in Meshery's web-based UI. +language: en +type: project +category: contributing +--- + +Prequisite reading: Contributing to Meshery UI + +## Contributing to Meshery UI - Notification Center + +## Notification Center + +The `NotificationCenter` component of Meshery UI Switching to Graphql subscriptions and implementing robust filtering. Events are persisted in Meshery Server and state management on client is done using Redux Toolkit and RTK. + +User-facing Features: + +- Robust filtering support inspired by GitHub's notification filtering style. + - Search is also included. +- Proper hierarchial presentation of error details, including probable cause and suggested remeditation. +- Suport for notification status (notifications can be marked as read and unread) + - *Future: Notifications can be acknowledged or resolved.* +- Event-based notification via Graphql subscription (provided by Meshery Server and any upstream components or externally managed systems, like Kubernetes) +- Infinite scroll for pagination. + +## State Management and Internal Details + +- The State on client is managed using `Redux Tooltik` and `Rtk-query` +- Update and Delete operations are optimistically handled. +- Network Request are cached and are invalidated when new events come or events are deleted/updated. +- Due to need for infinite scroll and optimistic update the events are stored globally in Redux. + +### Notification Filtering and Searching + +The Notfication Center includes a resusable component,, for sophisticated filtering of notifications based on their attributes. It adheres to the GitHub-style syntax for filtering, offering a straight-forward and adaptable way to filter and search notification details. + +The state for filtering is managed by a state machine created using a reducer, it supports multiple filters, suggestions and completions. + +### Notification Severities and Colors +Notification severities and colors are defined in the constants file, `ui/components/NotificationCenter/constants.js`. + +# TypingFilter Component Documentation + +The `TypingFilter` component is a customizable React component that enables real-time filtering and selection based on user input. +It provides a user-friendly interface for filtering data in your application. + +## Table of Contents + +- [Usage](#usage) +- [Props](#props) +- [Examples](#examples) + +## Usage + +The `TypingFilter` component is designed to provide an interactive filtering experience in your application. Here's how you can use it: + +```javascript +import React from 'react'; +import TypingFilter from './path-to-TypingFilter'; + +function MyComponent() { + // Define a filter schema that describes the available filter options. + const filterSchema = { + // Define your filter categories here + // Example: + SEVERITY: { + value: "severity", + description: "Filter by severity", + values: ["Low", "Medium", "High"], + multiple : true // default + }, + // Add more filter categories as needed + }; + + // Define a callback function to handle filter changes. + const handleFilterChange = (filteredData) => { + // Implement your logic to react to the filtered data. + // This function will be called when the user applies a filter. ( on presing enter in input) + console.log("Filtered data:", filteredData); + }; + + return ( +
      + + {/* Your other components */} +
      + ); +} + +export default MyComponent; +``` + +## Props + +The `TypingFilter` component accepts the following props: + +- `filterSchema` (object, required): An object that defines available filter options. Each property of this object represents a filter category with the following properties: + - `value` (string, required): The filter name used for filtering within the category. + - `description` (string, required): Description of the filter category. + - `type` (string, optional): The data type of the filter (e.g., "string", "number"). + - `values` (array, optional): Possible values for the filter. + +- `handleFilter` (function, required): A callback function that is called when the user applies a filter. This function receives the filtered data as an argument. + + +# Finite State Machine (FSM) for `TypingFilter` Component + +This README provides an overview of the Finite State Machine (FSM) implementation used to manage the state of the `TypingFilter` component. +The FSM is responsible for handling user interactions, such as selecting filters, entering values, and clearing the filter, within the component. + +## Table of Contents + +- [Overview](#overview) +- [State Definitions](#state-definitions) +- [Reducers](#reducers) +- [State Transitions](#state-transitions) +- [Initial State Handling](#initial-state-handling) + +## Overview + +The FSM implementation within the `TypingFilter` component ensures that user interactions are correctly processed and managed, resulting in a smooth and intuitive filtering experience. + +## State Definitions + +The FSM code defines three sets of constants to represent important elements within the state management: + +### 1. `FILTERING_STATE` + +Defines the possible states that the `TypingFilter` component can be in. These states include: +- `IDLE`: Represents the initial state when the component is not actively filtering. +- `SELECTING_FILTER`: Indicates that the user is selecting a filter. +- `SELECTING_VALUE`: Indicates that the user is entering a filter value. + +### 2. `FILTER_EVENTS` + +Represents the events that trigger state transitions within the FSM. Some of the events include: +- `START`: Initiates the filtering process. +- `SELECT`: Indicates the selection of a filter. +- `INPUT_CHANGE`: Represents a change in the filter input. +- `CLEAR`: Clears the filter. +- `EXIT`: Exits the filtering process. + +### 3. `Delimiter` + +Defines delimiters used to separate filter and value entries within the component. Delimiters include: +- `FILTER`: Separates multiple filters. +- `FILTER_VALUE`: Separates filters from their corresponding values. + +## Reducers + +The FSM implementation includes two key reducer functions: + +### 1. `commonReducer` + +This common reducer function handles events that are common across all states. It includes logic to handle "CLEAR" and "EXIT" events, which reset the component's state and clear any entered values. + +### 2. `filterSelectionReducer` + +The `filterSelectionReducer` is a specific reducer used to manage transitions between "SELECTING_FILTER" and "SELECTING_VALUE" states. It handles events related to selecting filters and entering values. The logic ensures that delimiters are appropriately added or removed when the user interacts with the filter. + +## State Transitions + +State transitions are managed based on user actions and the current state of the component. For example, when the user selects a filter, the state transitions from "SELECTING_FILTER" to "SELECTING_VALUE." When the user inputs values or clears the filter, the state transitions are managed accordingly. + +## Initial State Handling + +The FSM implementation includes handling for the initial state, where it listens for the "START" event to transition from "IDLE" to "SELECTING_FILTER." This ensures that the filtering process is initiated when the user interacts with the component. + + + +{% include code.html code="let svg = 'data:image/svg+xml;utf8,' + encodeURIComponent(svgFile);" %} + +{% include suggested-reading.html %} diff --git a/scripts/component_generation/dump.csv b/scripts/component_generation/dump.csv new file mode 100644 index 00000000000..0f398276fc3 --- /dev/null +++ b/scripts/component_generation/dump.csv @@ -0,0 +1 @@ +model,component_count,components From dc0a3cd89611e88b2529f7e73cd5c40d109942c9 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Fri, 15 Sep 2023 15:19:08 -0500 Subject: [PATCH 20/44] rm dump.csv Signed-off-by: Lee Calcote --- scripts/component_generation/dump.csv | 1 - 1 file changed, 1 deletion(-) delete mode 100644 scripts/component_generation/dump.csv diff --git a/scripts/component_generation/dump.csv b/scripts/component_generation/dump.csv deleted file mode 100644 index 0f398276fc3..00000000000 --- a/scripts/component_generation/dump.csv +++ /dev/null @@ -1 +0,0 @@ -model,component_count,components From cdae34974df47b88250656bbe2c0abefc6791117 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Fri, 15 Sep 2023 15:48:56 -0500 Subject: [PATCH 21/44] more docs Signed-off-by: Lee Calcote --- ...=> contributing-ui-notification-center.md} | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) rename docs/pages/project/contributing/{contributing-ui-notification-cener.md => contributing-ui-notification-center.md} (76%) diff --git a/docs/pages/project/contributing/contributing-ui-notification-cener.md b/docs/pages/project/contributing/contributing-ui-notification-center.md similarity index 76% rename from docs/pages/project/contributing/contributing-ui-notification-cener.md rename to docs/pages/project/contributing/contributing-ui-notification-center.md index 7146535a6a9..6e0f792ae1f 100644 --- a/docs/pages/project/contributing/contributing-ui-notification-cener.md +++ b/docs/pages/project/contributing/contributing-ui-notification-center.md @@ -8,15 +8,20 @@ type: project category: contributing --- -Prequisite reading: Contributing to Meshery UI +

      Prerequisite Reading

      +
      1. Contributing to Meshery UI
      +
      ## Contributing to Meshery UI - Notification Center -## Notification Center + The `NotificationCenter` component of Meshery UI Switching to Graphql subscriptions and implementing robust filtering. Events are persisted in Meshery Server and state management on client is done using Redux Toolkit and RTK. -User-facing Features: +#### User-facing Features - Robust filtering support inspired by GitHub's notification filtering style. - Search is also included. @@ -26,34 +31,30 @@ User-facing Features: - Event-based notification via Graphql subscription (provided by Meshery Server and any upstream components or externally managed systems, like Kubernetes) - Infinite scroll for pagination. -## State Management and Internal Details +#### State Management and Internal Details - The State on client is managed using `Redux Tooltik` and `Rtk-query` - Update and Delete operations are optimistically handled. - Network Request are cached and are invalidated when new events come or events are deleted/updated. - Due to need for infinite scroll and optimistic update the events are stored globally in Redux. -### Notification Filtering and Searching - -The Notfication Center includes a resusable component,, for sophisticated filtering of notifications based on their attributes. It adheres to the GitHub-style syntax for filtering, offering a straight-forward and adaptable way to filter and search notification details. - -The state for filtering is managed by a state machine created using a reducer, it supports multiple filters, suggestions and completions. - ### Notification Severities and Colors -Notification severities and colors are defined in the constants file, `ui/components/NotificationCenter/constants.js`. -# TypingFilter Component Documentation +Notification severities and colors are defined in the constants file, `ui/components/NotificationCenter/constants.js`. -The `TypingFilter` component is a customizable React component that enables real-time filtering and selection based on user input. -It provides a user-friendly interface for filtering data in your application. +### Notification Filtering and Searching -## Table of Contents +**Table of Contents** - [Usage](#usage) - [Props](#props) - [Examples](#examples) -## Usage +The Notfication Center includes a resusable component, `TypingFilter`, for sophisticated filtering and searching of notifications based on their attributes. It adheres to the GitHub-style syntax for filtering, offering a straight-forward and adaptable way to filter and search notification details. The `TypingFilter` component is a customizable React component that enables real-time filtering and selection based on user input. + +The state for filtering is managed by a state machine created using a reducer. `TypingFilter` supports multiple filters, suggestions and completions. + +### Usage The `TypingFilter` component is designed to provide an interactive filtering experience in your application. Here's how you can use it: @@ -96,7 +97,7 @@ function MyComponent() { export default MyComponent; ``` -## Props +### Props The `TypingFilter` component accepts the following props: @@ -109,12 +110,11 @@ The `TypingFilter` component accepts the following props: - `handleFilter` (function, required): A callback function that is called when the user applies a filter. This function receives the filtered data as an argument. -# Finite State Machine (FSM) for `TypingFilter` Component +## Finite State Machine (FSM) for `TypingFilter` Component -This README provides an overview of the Finite State Machine (FSM) implementation used to manage the state of the `TypingFilter` component. -The FSM is responsible for handling user interactions, such as selecting filters, entering values, and clearing the filter, within the component. +This section provides an overview of the Finite State Machine (FSM) implementation used to manage the state of the `TypingFilter` component. The FSM is responsible for handling user interactions, such as selecting filters, entering values, and clearing the filter, within the component. The FSM implementation within the `TypingFilter` component ensures that user interactions are correctly processed and managed, resulting in a smooth and intuitive filtering experience. -## Table of Contents +**Table of Contents** - [Overview](#overview) - [State Definitions](#state-definitions) @@ -122,22 +122,18 @@ The FSM is responsible for handling user interactions, such as selecting filters - [State Transitions](#state-transitions) - [Initial State Handling](#initial-state-handling) -## Overview - -The FSM implementation within the `TypingFilter` component ensures that user interactions are correctly processed and managed, resulting in a smooth and intuitive filtering experience. - -## State Definitions +### State Definitions The FSM code defines three sets of constants to represent important elements within the state management: -### 1. `FILTERING_STATE` +#### 1. `FILTERING_STATE` Defines the possible states that the `TypingFilter` component can be in. These states include: - `IDLE`: Represents the initial state when the component is not actively filtering. - `SELECTING_FILTER`: Indicates that the user is selecting a filter. - `SELECTING_VALUE`: Indicates that the user is entering a filter value. -### 2. `FILTER_EVENTS` +#### 2. `FILTER_EVENTS` Represents the events that trigger state transitions within the FSM. Some of the events include: - `START`: Initiates the filtering process. @@ -146,34 +142,30 @@ Represents the events that trigger state transitions within the FSM. Some of the - `CLEAR`: Clears the filter. - `EXIT`: Exits the filtering process. -### 3. `Delimiter` +#### 3. `Delimiter` Defines delimiters used to separate filter and value entries within the component. Delimiters include: - `FILTER`: Separates multiple filters. - `FILTER_VALUE`: Separates filters from their corresponding values. -## Reducers +### Reducers The FSM implementation includes two key reducer functions: -### 1. `commonReducer` +#### 1. `commonReducer` This common reducer function handles events that are common across all states. It includes logic to handle "CLEAR" and "EXIT" events, which reset the component's state and clear any entered values. -### 2. `filterSelectionReducer` +#### 2. `filterSelectionReducer` The `filterSelectionReducer` is a specific reducer used to manage transitions between "SELECTING_FILTER" and "SELECTING_VALUE" states. It handles events related to selecting filters and entering values. The logic ensures that delimiters are appropriately added or removed when the user interacts with the filter. -## State Transitions +### State Transitions State transitions are managed based on user actions and the current state of the component. For example, when the user selects a filter, the state transitions from "SELECTING_FILTER" to "SELECTING_VALUE." When the user inputs values or clears the filter, the state transitions are managed accordingly. -## Initial State Handling +### Initial State Handling The FSM implementation includes handling for the initial state, where it listens for the "START" event to transition from "IDLE" to "SELECTING_FILTER." This ensures that the filtering process is initiated when the user interacts with the component. - - -{% include code.html code="let svg = 'data:image/svg+xml;utf8,' + encodeURIComponent(svgFile);" %} - {% include suggested-reading.html %} From 0cbc2a86d8892b98e1b5e7499884b3f05ea97743 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Fri, 15 Sep 2023 18:36:34 -0500 Subject: [PATCH 22/44] read and unread icons Signed-off-by: Lee Calcote --- ui/assets/icons/ReadIcon.js | 21 +++++++++++++++++++++ ui/assets/icons/UnreadIcon.js | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 ui/assets/icons/ReadIcon.js create mode 100644 ui/assets/icons/UnreadIcon.js diff --git a/ui/assets/icons/ReadIcon.js b/ui/assets/icons/ReadIcon.js new file mode 100644 index 00000000000..6b18dd7ddae --- /dev/null +++ b/ui/assets/icons/ReadIcon.js @@ -0,0 +1,21 @@ +import React from "react"; + +const ReadIcon = ({ height, width, fill, style = {} }) => { + return ( + + + + + + ); +}; + +export default ReadIcon; \ No newline at end of file diff --git a/ui/assets/icons/UnreadIcon.js b/ui/assets/icons/UnreadIcon.js new file mode 100644 index 00000000000..3c2c89d8c0a --- /dev/null +++ b/ui/assets/icons/UnreadIcon.js @@ -0,0 +1,27 @@ +import React from "react"; + +const UnreadIcon = ({ height, width, fill, style = {} }) => { + return ( + + + + ); +}; + +export default UnreadIcon; From 95949a9d115219df709b5345903db626e356e59a Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 16 Sep 2023 12:51:33 +0530 Subject: [PATCH 23/44] Add visibility transition and change status icons Signed-off-by: aabidsofi19 --- ui/assets/icons/ReadIcon.js | 2 +- ui/assets/icons/UnreadIcon.js | 4 +- ui/components/NotificationCenter/index.js | 54 +++--- .../NotificationCenter/notification.js | 169 ++++++++++-------- ui/store/slices/events.js | 59 +++--- 5 files changed, 150 insertions(+), 138 deletions(-) diff --git a/ui/assets/icons/ReadIcon.js b/ui/assets/icons/ReadIcon.js index 6b18dd7ddae..c20a4482926 100644 --- a/ui/assets/icons/ReadIcon.js +++ b/ui/assets/icons/ReadIcon.js @@ -2,7 +2,7 @@ import React from "react"; const ReadIcon = ({ height, width, fill, style = {} }) => { return ( - { { ); }; -export default UnreadIcon; +export default UnreadIcon; \ No newline at end of file diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index 3e36dcf52e7..e730d5ee5ce 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import IconButton from "@material-ui/core/IconButton"; import { Provider, useDispatch, useSelector } from "react-redux"; import NoSsr from "@material-ui/core/NoSsr"; -import { Drawer, Tooltip, Divider, ClickAwayListener, Typography, alpha, Chip, Button, Badge, CircularProgress, Box, useTheme } from "@material-ui/core"; +import { Drawer, Divider, ClickAwayListener, Typography, alpha, Chip, Button, Badge, CircularProgress, Box, useTheme } from "@material-ui/core"; import Filter from "./filter"; import BellIcon from "../../assets/icons/BellIcon.js" import { iconMedium } from "../../css/icons.styles"; @@ -123,26 +123,24 @@ const Header = ({ handleFilter, handleClose }) => { const Loading = () => { return ( - + ) } -const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { +const EventsView = ({ handleLoadNextPage, isFetching, hasMore }) => { const events = useSelector(selectEvents) // const page = useSelector((state) => state.events.current_view.page); const lastEventRef = useRef(null) const intersectionObserver = useRef(new IntersectionObserver((entries) => { - if (isLoading && !hasMore) { + if (isFetching && !hasMore) { return } const firstEntry = entries[0] if (firstEntry.isIntersecting) { handleLoadNextPage() - - console.log("intersecting") } }, { threshold : 1 })) @@ -161,15 +159,15 @@ const EventsView = ({ handleLoadNextPage, isLoading, hasMore }) => { return ( <> {events.map((event, idx) =>
      - +
      )} {events.length === 0 && }
      - {!Loading && hasMore && } + {!isFetching && hasMore && }
      - {isLoading && } + {isFetching && } ) } @@ -222,7 +220,7 @@ const MesheryNotification = () => { const [anchorEl, setAnchorEl] = useState(null); const dispatch = useDispatch() const isNotificationCenterOpen = useSelector((state) => state.events.isNotificationCenterOpen); - const [fetchEvents, { isLoading }] = useLazyGetEventsQuery() + const [fetchEvents, { isFetching }] = useLazyGetEventsQuery() const hasMore = useSelector((state) => state.events.current_view.has_more); useEffect(() => { @@ -258,24 +256,22 @@ const MesheryNotification = () => { return (
      - - { - e.preventDefault(); - setAnchorEl(e.currentTarget); - }} - onMouseLeave={(e) => { - e.preventDefault(); - setAnchorEl(null); - }} - > - - - + { + e.preventDefault(); + setAnchorEl(e.currentTarget); + }} + onMouseLeave={(e) => { + e.preventDefault(); + setAnchorEl(null); + }} + > + +
      {
      - +
      diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js index 0a8b9562251..f32e528cd4b 100644 --- a/ui/components/NotificationCenter/notification.js +++ b/ui/components/NotificationCenter/notification.js @@ -1,10 +1,9 @@ import * as React from 'react'; -import { Avatar, Box, Button, Collapse, Divider, Grid, Hidden, IconButton, Popover, Tooltip, Typography, alpha, useTheme } from "@material-ui/core" +import { Avatar, Box, Button, Collapse, Grid, Hidden, IconButton, Popover, Slide, Tooltip, Typography, alpha, useTheme } from "@material-ui/core" import { makeStyles } from "@material-ui/core" import { SEVERITY_STYLE, STATUS } from "./constants" import { iconLarge, iconMedium } from "../../css/icons.styles" import { MoreVert } from "@material-ui/icons" -// import { Avatar } from "@mui/material" import FacebookIcon from "../../assets/icons/FacebookIcon" import LinkedInIcon from "../../assets/icons/LinkedInIcon" import TwitterIcon from "../../assets/icons/TwitterIcon" @@ -12,11 +11,12 @@ import ShareIcon from "../../assets/icons/ShareIcon" import DeleteIcon from "../../assets/icons/DeleteIcon" import moment from 'moment'; import { useUpdateStatusMutation, useDeleteEventMutation } from "../../rtk-query/notificationCenter" -import { useDispatch } from 'react-redux'; -import { changeEventStatus, deleteEvent } from '../../store/slices/events'; +import { useDispatch, useSelector } from 'react-redux'; +import { changeEventStatus, deleteEvent, selectEventById } from '../../store/slices/events'; import { useGetUserByIdQuery } from '../../rtk-query/user'; import { FacebookShareButton, LinkedinShareButton, TwitterShareButton } from 'react-share'; -import DoneIcon from '../../assets/icons/DoneIcon'; +import ReadIcon from '../../assets/icons/ReadIcon'; +import UnreadIcon from '../../assets/icons/UnreadIcon'; const useStyles = makeStyles(() => ({ root : (props) => ({ @@ -95,7 +95,7 @@ const useMenuStyles = makeStyles((theme) => { }, button : { - padding : "0rem", + padding : "0.2rem", display : "flex", alignItems : "center", justifyContent : "start", @@ -116,7 +116,7 @@ const formatTimestamp = (utcTimestamp) => { return moment(utcTimestamp).fromNow() } -function BasicMenu({ event }) { +function BasicMenu({ event } ) { const classes = useMenuStyles() const [anchorEl, setAnchorEl] = React.useState(null); @@ -132,9 +132,15 @@ function BasicMenu({ event }) { setAnchorEl(null); }; + const [isSocialShareOpen, setIsSocialShareOpen] = React.useState(false) + const toggleSocialShare = (e) => { + e.stopPropagation() + setIsSocialShareOpen(prev => !prev) + } + const theme = useTheme() return ( -
      +
      e.stopPropagation()} >
      - - - Share - - - - - - - - - - - - + + + + + + + + + + + + + + +
      @@ -219,7 +228,10 @@ export const ChangeStatus = ({ event }) => { return (
      @@ -235,7 +247,11 @@ const BulletList = ({ items }) => {
    } -export const Notification = ({ event }) => { + + +export const Notification = ({ event_id }) => { + const event = useSelector(state => selectEventById(state, event_id)) + const isVisible = event.is_visible === undefined ? true : event.is_visible const severityStyles = SEVERITY_STYLE[event.severity] const classes = useStyles({ notificationColor : severityStyles.color, @@ -258,68 +274,69 @@ export const Notification = ({ event }) => { return ( -
    - - - - - - {event.description} - - - - {formatTimestamp(event.created_at)} + +
    + + + - - - - - - - - - - - - - {event.user_id && - - - - } - {event.system_id && - - - - } - + + {event.description} + + + + {formatTimestamp(event.created_at)} + + + + + - - -
    - -
    - + + + + + + + {event.user_id && + + + + } + {event.system_id && + + + + } + + + + + +
    + +
    + +
    -
    - - 0 ? 6 : 12}> - - - 0 ? 6 : 12} > - + + 0 ? 6 : 12}> + + + 0 ? 6 : 12} > + + - - -
    +
    +
    +
    ) } - const NestedData = ({ heading, data, classes }) => { if (!data || data?.length == 0) return null diff --git a/ui/store/slices/events.js b/ui/store/slices/events.js index b0290ed30c6..840fa0679aa 100644 --- a/ui/store/slices/events.js +++ b/ui/store/slices/events.js @@ -2,27 +2,27 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit' import { SEVERITY, STATUS } from '../../components/NotificationCenter/constants' const initialState = { - current_view: { - page: 1, - page_size: 10, - filters: { - initial: true, + current_view : { + page : 1, + page_size : 10, + filters : { + initial : true, }, - has_more: true, + has_more : true, }, - isNotificationCenterOpen: false, + isNotificationCenterOpen : false, } const defaultEventProperties = { - severity: SEVERITY.INFO, - status: STATUS.UNREAD, + severity : SEVERITY.INFO, + status : STATUS.UNREAD, } const eventsEntityAdapter = createEntityAdapter({ - selectId: (event) => event.id, + selectId : (event) => event.id, //sort based on update_at timestamp(utc) - sortComparer: (a, b) => { + sortComparer : (a, b) => { if (b?.created_at?.localeCompare && a?.created_at?.localeCompare) { return b.created_at?.localeCompare(a.created_at) } @@ -31,15 +31,15 @@ const eventsEntityAdapter = createEntityAdapter({ }) export const eventsSlice = createSlice({ - name: 'events', - initialState: eventsEntityAdapter.getInitialState(initialState), - reducers: { + name : 'events', + initialState : eventsEntityAdapter.getInitialState(initialState), + reducers : { - clearEvents: (state) => { + clearEvents : (state) => { state.events = [] }, - setEvents: (state, action) => { + setEvents : (state, action) => { // state.events = action.payload || [] eventsEntityAdapter.removeAll(state) eventsEntityAdapter.addMany(state, action.payload) @@ -48,39 +48,39 @@ export const eventsSlice = createSlice({ }, - pushEvents: (state, action) => { + pushEvents : (state, action) => { // state.events = [...state.events, ...action.payload] eventsEntityAdapter.addMany(state, action.payload) state.current_view.has_more = action.payload.length == 0 ? false : true }, - pushEvent: (state, action) => { + pushEvent : (state, action) => { const event = { ...action.payload, - severity: action.payload?.severity?.trim() || defaultEventProperties.severity, - status: action.payload?.status?.trim() || defaultEventProperties.status, + severity : action.payload?.severity?.trim() || defaultEventProperties.severity, + status : action.payload?.status?.trim() || defaultEventProperties.status, } eventsEntityAdapter.addOne(state, event) // state.events = [event, ...state.events] }, - updateEvent: eventsEntityAdapter.updateOne, - deleteEvent: eventsEntityAdapter.removeOne, + updateEvent : eventsEntityAdapter.updateOne, + deleteEvent : eventsEntityAdapter.removeOne, - clearCurrentView: (state) => { + clearCurrentView : (state) => { state.current_view = initialState.current_view state.events = [] }, - setCurrentView: (state, action) => { + setCurrentView : (state, action) => { state.current_view = action.payload }, - toggleNotificationCenter: (state) => { + toggleNotificationCenter : (state) => { state.isNotificationCenterOpen = !state.isNotificationCenterOpen }, - closeNotificationCenter: (state) => { + closeNotificationCenter : (state) => { state.isNotificationCenterOpen = false }, @@ -90,7 +90,7 @@ export const eventsSlice = createSlice({ // Action creators are generated for each case reducer function export const { pushEvent, clearEvents, setEvents, clearCurrentView, - pushEvents, setCurrentView, updateEvent, deleteEvent: removeEvent, + pushEvents, setCurrentView, updateEvent, deleteEvent : removeEvent, toggleNotificationCenter, closeNotificationCenter } = eventsSlice.actions @@ -123,12 +123,13 @@ export const loadNextPage = (fetch) => async (dispatch, getState) => { } export const changeEventStatus = (mutator, id, status) => async (dispatch) => { - dispatch(updateEvent({ id, changes: { status } })) + dispatch(updateEvent({ id, changes : { status } })) mutator({ id, status }) } export const deleteEvent = (mutator, id) => async (dispatch) => { - dispatch(removeEvent(id)) + + dispatch(updateEvent({ id, changes : { is_visible : false } })) mutator({ id }) } From ea6463740be89bd39cf4c1c2f682a75405ac0340 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 16 Sep 2023 14:50:03 +0530 Subject: [PATCH 24/44] remove from view on status change and add tooltips Signed-off-by: aabidsofi19 --- ui/components/NotificationCenter/constants.js | 4 +-- ui/components/NotificationCenter/index.js | 27 ++++++++++--------- ui/store/slices/events.js | 9 +++++-- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/ui/components/NotificationCenter/constants.js b/ui/components/NotificationCenter/constants.js index f63b8b0b7a0..e5a1a1ae893 100644 --- a/ui/components/NotificationCenter/constants.js +++ b/ui/components/NotificationCenter/constants.js @@ -1,8 +1,8 @@ import { NOTIFICATIONCOLORS } from "../../themes" import AlertIcon from "../../assets/icons/AlertIcon"; -import ArchiveIcon from "../../assets/icons/ArchiveIcon"; import ErrorIcon from "../../assets/icons/ErrorIcon.js" import { Colors } from "../../themes/app"; +import ReadIcon from "../../assets/icons/ReadIcon"; export const SEVERITY = { INFO : "informational", @@ -18,7 +18,7 @@ export const STATUS = { export const STATUS_STYLE = { [STATUS.READ] : { - icon : ArchiveIcon, + icon : ReadIcon, color : Colors.charcoal } } diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index e730d5ee5ce..f4399af8d1c 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import IconButton from "@material-ui/core/IconButton"; import { Provider, useDispatch, useSelector } from "react-redux"; import NoSsr from "@material-ui/core/NoSsr"; -import { Drawer, Divider, ClickAwayListener, Typography, alpha, Chip, Button, Badge, CircularProgress, Box, useTheme } from "@material-ui/core"; +import { Drawer, Divider, ClickAwayListener, Typography, alpha, Chip, Button, Badge, CircularProgress, Box, useTheme, Tooltip } from "@material-ui/core"; import Filter from "./filter"; import BellIcon from "../../assets/icons/BellIcon.js" import { iconMedium } from "../../css/icons.styles"; @@ -56,7 +56,7 @@ const NavbarNotificationIcon = () => { } -const NotificationCountChip = ({ classes, notificationStyle, count, handleClick }) => { +const NotificationCountChip = ({ classes, notificationStyle, count,type, handleClick }) => { const chipStyles = { fill : notificationStyle.color, height : "20px", @@ -64,14 +64,16 @@ const NotificationCountChip = ({ classes, notificationStyle, count, handleClick } count = Number(count).toLocaleString('en', { useGrouping : true }) return ( - + + + ) } @@ -109,11 +111,13 @@ const Header = ({ handleFilter, handleClose }) => { {Object.values(SEVERITY).map(severity => ( onClickSeverity(severity)} notificationStyle={SEVERITY_STYLE[severity]} + type={`Unread ${severity}(s)`} count={getSeverityCount(count_by_severity_level, severity)} />) )} onClickStatus(STATUS.READ)} + type={STATUS.READ} count={archivedCount} />
    @@ -165,9 +169,8 @@ const EventsView = ({ handleLoadNextPage, isFetching, hasMore }) => { {events.length === 0 && }
    - {!isFetching && hasMore && }
    - {isFetching && } + {isFetching && hasMore && } ) } diff --git a/ui/store/slices/events.js b/ui/store/slices/events.js index 840fa0679aa..14cc62941a9 100644 --- a/ui/store/slices/events.js +++ b/ui/store/slices/events.js @@ -122,8 +122,13 @@ export const loadNextPage = (fetch) => async (dispatch, getState) => { dispatch(loadEvents(fetch, currentView.page + 1, currentView.filters)) } -export const changeEventStatus = (mutator, id, status) => async (dispatch) => { - dispatch(updateEvent({ id, changes : { status } })) +export const changeEventStatus = (mutator, id, status) => async (dispatch,getState) => { + const currentView = getState().events.current_view + + dispatch(updateEvent({ id, changes : { + status, + is_visible : currentView?.filters?.status ? false : true //if status filter is applied, then remove the event from view + } })) mutator({ id, status }) } From 3c73dc5892fe9c5eb85942536729238ce90cd533 Mon Sep 17 00:00:00 2001 From: aabidsofi19 Date: Sat, 16 Sep 2023 22:06:51 +0530 Subject: [PATCH 25/44] Swap icons Signed-off-by: aabidsofi19 --- ui/assets/icons/ReadIcon.js | 14 +++++++++----- ui/assets/icons/UnreadIcon.js | 14 +++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ui/assets/icons/ReadIcon.js b/ui/assets/icons/ReadIcon.js index c20a4482926..f47b1482fd2 100644 --- a/ui/assets/icons/ReadIcon.js +++ b/ui/assets/icons/ReadIcon.js @@ -2,19 +2,23 @@ import React from "react"; const ReadIcon = ({ height, width, fill, style = {} }) => { return ( - - + - - ); }; diff --git a/ui/assets/icons/UnreadIcon.js b/ui/assets/icons/UnreadIcon.js index f5ca1b0dfd9..4c459448049 100644 --- a/ui/assets/icons/UnreadIcon.js +++ b/ui/assets/icons/UnreadIcon.js @@ -2,23 +2,19 @@ import React from "react"; const UnreadIcon = ({ height, width, fill, style = {} }) => { return ( - - + + + ); }; From b0b9cf760a400e50e446e62579c1f8831e085d71 Mon Sep 17 00:00:00 2001 From: Daniel Kiptoon Date: Sun, 17 Sep 2023 17:41:53 +0300 Subject: [PATCH 26/44] remove option --adapter in favour of a plain adapters name Signed-off-by: Daniel Kiptoon --- .github/workflows/mesheryctl-e2e.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mesheryctl-e2e.yaml b/.github/workflows/mesheryctl-e2e.yaml index 583725cf01e..5f183440238 100644 --- a/.github/workflows/mesheryctl-e2e.yaml +++ b/.github/workflows/mesheryctl-e2e.yaml @@ -61,7 +61,7 @@ jobs: - name: Deploy Service Mesh run: | cat ~/auth.json - echo | mesheryctl mesh deploy --adapter meshery-istio --namespace istio-system -t ~/auth.json ISTIO + echo | mesheryctl mesh deploy istio --namespace istio-system -t ~/auth.json ISTIO sleep 50 shell: '/bin/bash {0}' @@ -114,7 +114,7 @@ jobs: - name: Deploy Service Mesh run: | - echo | mesheryctl mesh deploy --adapter meshery-istio --namespace istio-system -t ~/auth.json ISTIO + echo | mesheryctl mesh deploy istio --namespace istio-system -t ~/auth.json ISTIO sleep 50 shell: '/bin/bash {0}' From 6182547ea2aa0a489bf2f482b6b14e4c905cc1a2 Mon Sep 17 00:00:00 2001 From: EraKin575 Date: Sun, 17 Sep 2023 21:03:13 +0530 Subject: [PATCH 27/44] changed MeshAdapterConfigComponent Signed-off-by: EraKin575 --- ui/components/MeshAdapterConfigComponent.js | 439 +++++++++++--------- 1 file changed, 237 insertions(+), 202 deletions(-) diff --git a/ui/components/MeshAdapterConfigComponent.js b/ui/components/MeshAdapterConfigComponent.js index ffee074f82b..3468c66deb2 100644 --- a/ui/components/MeshAdapterConfigComponent.js +++ b/ui/components/MeshAdapterConfigComponent.js @@ -1,8 +1,8 @@ -import React from "react"; +import React, { useEffect, useState, useRef } from "react"; import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; + import Grid from "@material-ui/core/Grid"; -import { NoSsr, Chip, Button, TextField, Tooltip, Avatar } from "@material-ui/core"; +import { NoSsr, Chip, Button, TextField, Tooltip, Avatar, makeStyles } from "@material-ui/core"; import blue from "@material-ui/core/colors/blue"; import { connect } from "react-redux"; import { bindActionCreators } from "redux"; @@ -11,17 +11,68 @@ import ReactSelectWrapper from "./ReactSelectWrapper"; import { updateAdaptersInfo, updateProgress } from "../lib/store"; import dataFetch from "../lib/data-fetch"; import changeAdapterState from './graphql/mutations/AdapterStatusMutation'; -import { withNotify } from "../utils/hooks/useNotification"; +import { withNotify,useNotification } from "../utils/hooks/useNotification"; import { EVENT_TYPES } from "../lib/event-types"; import BadgeAvatars from './CustomAvatar'; -const styles = (theme) => ({ +// const styles = (theme) => ({ +// wrapperClass : { +// padding : theme.spacing(5), +// backgroundColor : theme.palette.secondary.elevatedComponents, +// borderBottomLeftRadius : theme.spacing(1), +// borderBottomRightRadius : theme.spacing(1), +// marginTop : theme.spacing(2), +// }, +// buttons : { +// display : "flex", +// justifyContent : "flex-end", paddingTop : "2rem" +// }, +// button : { +// marginLeft : theme.spacing(1), +// }, +// margin : { margin : theme.spacing(1), }, +// alreadyConfigured : { +// textAlign : "center", +// padding : theme.spacing(20), +// }, +// colorSwitchBase : { +// color : blue[300], +// "&$colorChecked" : { +// color : blue[500], +// "& + $colorBar" : { backgroundColor : blue[500], }, +// }, +// }, +// colorBar : {}, +// colorChecked : {}, +// fileLabel : { width : "100%", }, +// fileLabelText : {}, +// inClusterLabel : { paddingRight : theme.spacing(2), }, +// alignCenter : { textAlign : "center", }, +// alignRight : { +// textAlign : "right", +// marginBottom : theme.spacing(2), +// }, +// fileInputStyle : { opacity : "0.01", }, +// // icon : { width : theme.spacing(2.5), }, +// icon : { +// width : 20, +// height : 20 +// }, +// istioIcon : { width : theme.spacing(1.5), }, +// chip : { +// marginRight : theme.spacing(1), +// marginBottom : theme.spacing(1), +// } +// }); + +const useStyles = makeStyles((theme) => ({ wrapperClass : { padding : theme.spacing(5), backgroundColor : theme.palette.secondary.elevatedComponents, borderBottomLeftRadius : theme.spacing(1), borderBottomRightRadius : theme.spacing(1), marginTop : theme.spacing(2), + marginBottom : theme.spacing(2), }, buttons : { display : "flex", @@ -41,6 +92,7 @@ const styles = (theme) => ({ color : blue[500], "& + $colorBar" : { backgroundColor : blue[500], }, }, + "&$colorChecked + $colorBar" : { backgroundColor : blue[500], }, }, colorBar : {}, colorChecked : {}, @@ -63,8 +115,7 @@ const styles = (theme) => ({ marginRight : theme.spacing(1), marginBottom : theme.spacing(1), } -}); - +})); const STATUS = { DEPLOYED : "DEPLOYED", UNDEPLOYED : "UNDEPLOYED", @@ -72,43 +123,48 @@ const STATUS = { UNDEPLOYING : "UNDEPLOYING", } -class MeshAdapterConfigComponent extends React.Component { - constructor(props) { - super(props); - const { meshAdapters } = props; - this.labelRef = React.createRef(); - this.state = { - meshAdapters, - setAdapterURLs : [], - availableAdapters : [], - ts : new Date(), - meshLocationURLError : false, - selectedAvailableAdapterError : false, - adapterStates : {}, - }; - } - - static getDerivedStateFromProps(props, state) { - const { meshAdapters, meshAdaptersts } = props; - // if(meshAdapters.sort().join(',') !== state.meshAdapters.sort().join(',')){ - if (meshAdaptersts > state.ts) { - return { - meshAdapters, - ts : meshAdaptersts - }; +const MeshAdapterConfigComponent = (props) => { + + const labelRef = useRef(null) + // this.state = { + // meshAdapters, + // setAdapterURLs : [], + // availableAdapters : [], + // ts : new Date(), + // meshLocationURLError : false, + // selectedAvailableAdapterError : false, + // adapterStates : {}, + // }; + const [meshAdapters, setMeshAdapters] = useState(props.meshAdapters); + const [setAdapterURLs, setSetAdapterURLs] = useState([]); + const [availableAdapters, setAvailableAdapters] = useState([]); + const [ts, setTs] = useState(props.meshAdaptersts); + const [meshLocationURLError, setMeshLocationURLError] = useState(false); + const [selectedAvailableAdapterError, setSelectedAvailableAdapterError] = useState(false); + const [adapterStates, setAdapterStates] = useState(props.meshAdapterStates); + const [meshLocationURL,setMeshLocationURL] = useState(); + const [meshDeployURL,setMeshDeployURL] = useState(); + const [meshDeployURLError, setMeshDeployURLError] = useState(); + const [selectedAvailableAdapter, setSelectedAvailableAdapter] = useState(); + const classes = useStyles(); + const { notify } = useNotification(); + + useEffect(() => { + if (props.meshAdaptersts > ts) { + setMeshAdapters(props.meshAdapters); + setTs(props.meshAdaptersts); } - return {}; - } + }, [props.meshAdaptersts, ts]); - componentDidMount = () => { - this.fetchSetAdapterURLs(); - this.fetchAvailableAdapters(); - this.setAdapterStates(); - } + useEffect(() => { + fetchSetAdapterURLs(); + fetchAvailableAdapters(); + setAdapterStatesFunction(); + }, []); + + const fetchSetAdapterURLs = () => { + updateProgress({ showProgress : true }); - fetchSetAdapterURLs = () => { - const self = this; - this.props.updateProgress({ showProgress : true }); dataFetch( "/api/system/adapters", { @@ -116,22 +172,22 @@ class MeshAdapterConfigComponent extends React.Component { credentials : "include", }, (result) => { - this.props.updateProgress({ showProgress : false }); + updateProgress({ showProgress : false }); if (typeof result !== "undefined") { const options = result.map((res) => ({ value : res.adapter_location, label : res.adapter_location, })); - this.setState({ setAdapterURLs : options }); + setSetAdapterURLs(options); } }, - self.handleError("Unable to fetch available adapters") + handleError("Unable to fetch available adapters") ); }; - fetchAvailableAdapters = () => { - const self = this; - this.props.updateProgress({ showProgress : true }); + const fetchAvailableAdapters = () => { + updateProgress({ showProgress : true }); + dataFetch( "/api/system/availableAdapters", { @@ -139,34 +195,31 @@ class MeshAdapterConfigComponent extends React.Component { credentials : "include", }, (result) => { - this.props.updateProgress({ showProgress : false }); + updateProgress({ showProgress : false }); if (typeof result !== "undefined") { const options = result.map((res) => ({ value : res.adapter_location, label : res.name, })); - this.setState({ availableAdapters : options }); + setAvailableAdapters(options); } }, - self.handleError("Unable to fetch available adapters") + handleError("Unable to fetch available adapters") ); }; - setAdapterStates = () => { - const { meshAdapters } = this.state; + const setAdapterStatesFunction = () => { const initialAdapterStates = {}; meshAdapters.forEach((adapter) => { - const lable = adapter.name.toUpperCase() - initialAdapterStates[lable] = STATUS.UNDEPLOYED; + const label = adapter.name.toUpperCase(); + initialAdapterStates[label] = STATUS.UNDEPLOYED; }); - this.setState({ - adapterStates : initialAdapterStates, - }); - } + setAdapterStates(initialAdapterStates); + }; - getStatusColor = (status) => { + const getStatusColor = (status) => { if (status === STATUS.DEPLOYED) { return "#00B39F"; } else if (status === STATUS.UNDEPLOYED) { @@ -178,53 +231,58 @@ class MeshAdapterConfigComponent extends React.Component { } } - handleChange = (name) => (event) => { - if (name === "meshLocationURL" && event.target.value !== "") { - this.setState({ meshLocationURLError : false }); - } - this.setState({ [name] : event.target.value }); - }; - - handleMeshLocURLChange = (newValue) => { + // const handleChange = (name) => (event) => { + // if (name === "meshLocationURL" && event.target.value !== "") { + // setMeshLocationURLError(false); + // } + + // setStateValues(prevState => ({ + // ...prevState, + // [name] : event.target.value + // })); + // }; + const handleMeshLocURLChange = (newValue) => { // console.log(newValue); // console.log(`action: ${actionMeta.action}`); // console.groupEnd(); + if (typeof newValue !== "undefined") { - this.setState({ meshLocationURL : newValue, meshLocationURLError : false }); + setMeshLocationURL(newValue); + setMeshLocationURLError(false); } }; - - handleDeployPortChange = (newValue) => { + const handleDeployPortChange = (newValue) => { if (typeof newValue !== "undefined") { console.log("port change to " + (newValue.value)) - this.setState({ meshDeployURL : newValue.value, meshDeployURLError : false }); + setMeshDeployURL(newValue.value); + setMeshDeployURLError(false); } - } + }; - handleAvailableAdapterChange = (newValue) => { + const handleAvailableAdapterChange = (newValue) => { if (typeof newValue !== "undefined") { // Trigger label animation manually - this.labelRef.current.querySelector('label').classList.add('MuiInputLabel-shrink'); - this.setState({ selectedAvailableAdapter : newValue, selectedAvailableAdapterError : false }); + labelRef.current.querySelector('label').classList.add('MuiInputLabel-shrink'); + setSelectedAvailableAdapter(newValue); + setSelectedAvailableAdapterError(false); + if (newValue !== null) { - this.setState({ meshDeployURL : newValue.value, meshDeployURLError : false }); + setMeshDeployURL(newValue.value); + setMeshDeployURLError(false); } } }; - handleSubmit = () => { - const { meshLocationURL } = this.state; + const handleSubmit = () => { if (!meshLocationURL || !meshLocationURL.value || meshLocationURL.value === "") { - this.setState({ meshLocationURLError : true }); + setMeshLocationURLError(true); return; } - this.submitConfig(); + submitConfig(); }; - - submitConfig = () => { - const { meshLocationURL } = this.state; + const submitConfig = () => { const data = { meshLocationURL : meshLocationURL.value }; @@ -232,8 +290,8 @@ class MeshAdapterConfigComponent extends React.Component { .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`) .join("&"); - this.props.updateProgress({ showProgress : true }); - const self = this; + updateProgress({ showProgress : true }); + dataFetch( "/api/system/adapter/manage", { @@ -244,85 +302,80 @@ class MeshAdapterConfigComponent extends React.Component { body : params, }, (result) => { - self.props.updateProgress({ showProgress : false }); + updateProgress({ showProgress : false }); if (typeof result !== "undefined") { - self.setState({ meshAdapters : result, meshLocationURL : "" }); - const notify = self.props.notify; + // self.setState({ meshAdapters : result, meshLocationURL : "" }); + setMeshAdapters(result); + setMeshLocationURL(""); + notify({ message : "Adapter was configured!", event_type : EVENT_TYPES.SUCCESS }) - self.props.updateAdaptersInfo({ meshAdapters : result }); - self.fetchSetAdapterURLs(); + updateAdaptersInfo({ meshAdapters : result }); + fetchSetAdapterURLs(); } }, - self.handleError("Adapter was not configured due to an error") + handleError("Adapter was not configured due to an error") ); }; + const handleDelete = (adapterLoc) => () => { + updateProgress({ showProgress : true }); - handleDelete = (adapterLoc) => () => { - // const { meshAdapters } = this.state; - this.props.updateProgress({ showProgress : true }); - const self = this; dataFetch( `/api/system/adapter/manage?adapter=${encodeURIComponent(adapterLoc)}`, { - method : "DELETE", credentials : "include", }, (result) => { - this.props.updateProgress({ showProgress : false }); + updateProgress({ showProgress : false }); if (typeof result !== "undefined") { - this.setState({ meshAdapters : result }); - const notify = self.props.notify; - notify({ message : "Adapter was removed!", event_type : EVENT_TYPES.SUCCESS }) - this.props.updateAdaptersInfo({ meshAdapters : result }); + setMeshAdapters(result); + notify({ message : "Adapter was removed!", event_type : EVENT_TYPES.SUCCESS }); + updateAdaptersInfo({ meshAdapters : result }); } }, - self.handleError("Adapter was not removed due to an error") + handleError("Adapter was not removed due to an error") ); }; - handleClick = (adapterLoc) => () => { - // const { meshAdapters } = this.state; - this.props.updateProgress({ showProgress : true }); - const self = this; + const handleClick = (adapterLoc) => () => { + updateProgress({ showProgress : true }); + dataFetch( `/api/system/adapters?adapter=${encodeURIComponent(adapterLoc)}`, { credentials : "include", }, (result) => { - this.props.updateProgress({ showProgress : false }); + updateProgress({ showProgress : false }); if (typeof result !== "undefined") { - const notify = self.props.notify; - notify({ message : "Adapter was pinged!", event_type : EVENT_TYPES.SUCCESS }) + notify({ message : "Adapter was pinged!", event_type : EVENT_TYPES.SUCCESS }); } }, - self.handleError("error") + handleError("error") ); }; - handleAdapterDeploy = () => { - const { selectedAvailableAdapter, meshDeployURL } = this.state; + + + const handleAdapterDeploy = () => { if (!selectedAvailableAdapter || !selectedAvailableAdapter.value || selectedAvailableAdapter.value === "") { - this.setState({ selectedAvailableAdapterError : true }); + setSelectedAvailableAdapterError(true); return; } const adapterLabel = selectedAvailableAdapter.label.replace(/^meshery-/, '').toUpperCase(); - this.setState(prevState => ({ - adapterStates : { - ...prevState.adapterStates, - [adapterLabel] : STATUS.DEPLOYING, - }, + setAdapterStates(prevState => ({ + ...prevState, + [adapterLabel] : STATUS.DEPLOYING, })); if (!meshDeployURL || meshDeployURL === "") { - console.log(meshDeployURL) - this.setState({ meshDeployURLError : true }); + console.log(meshDeployURL); + setMeshDeployURLError(true); return; } - this.props.updateProgress({ showProgress : true }); + updateProgress({ showProgress : true }); const variables = { status : "ENABLED", @@ -331,70 +384,61 @@ class MeshAdapterConfigComponent extends React.Component { }; changeAdapterState((response, errors) => { - this.props.updateProgress({ showProgress : false }); + updateProgress({ showProgress : false }); if (errors !== undefined) { - this.handleError("Unable to Deploy adapter"); - this.setState(prevState => ({ - adapterStates : { - ...prevState.adapterStates, - [adapterLabel] : STATUS.UNDEPLOYED, - }, - })) + handleError("Unable to Deploy adapter"); + setAdapterStates(prevState => ({ + ...prevState, + [adapterLabel] : STATUS.UNDEPLOYED, + })); } - this.setState(prevState => ({ - adapterStates : { - ...prevState.adapterStates, - [adapterLabel] : STATUS.DEPLOYED, - }, - })) - const notify = this.props.notify; - notify({ message : "Adapter " + response.adapterStatus.toLowerCase(), event_type : EVENT_TYPES.SUCCESS }) + setAdapterStates(prevState => ({ + ...prevState, + [adapterLabel] : STATUS.DEPLOYED, + })); + notify({ message : "Adapter " + response.adapterStatus.toLowerCase(), event_type : EVENT_TYPES.SUCCESS }); }, variables); }; - handleAdapterUndeploy = () => { - const { meshLocationURL, availableAdapters } = this.state; - + const handleAdapterUndeploy = () => { if (!meshLocationURL || !meshLocationURL.value || meshLocationURL.value === "") { - this.setState({ meshLocationURLError : true }); + setMeshLocationURLError(true); return; } - this.props.updateProgress({ showProgress : true }); + updateProgress({ showProgress : true }); - const targetPort = function getTargetPort(location) { - if (!location.value) { - return null + const targetPort = (() => { + if (!meshLocationURL.value) { + return null; } - if (location.value.includes(":")) { - return location.value.split(":")[1] + if (meshLocationURL.value.includes(":")) { + return meshLocationURL.value.split(":")[1]; } - return location.value; - }(meshLocationURL) + return meshLocationURL.value; + })(); - const adapterName = function getAdapterName(location) { - if (!location.value) { - return null + const adapterName = (() => { + if (!meshLocationURL.value) { + return null; } - if (location.value.includes(":")) { - return location.value.split(":")[0] + if (meshLocationURL.value.includes(":")) { + return meshLocationURL.value.split(":")[0]; } - return location.value; - }(meshLocationURL) + return meshLocationURL.value; + })(); const adapterLabel = (availableAdapters.find(adapter => adapter.value === targetPort)?.label || "").replace(/^meshery-/, '').toUpperCase(); - this.setState(prevState => ({ - adapterStates : { - ...prevState.adapterStates, - [adapterLabel] : STATUS.UNDEPLOYING, - }, + setAdapterStates(prevState => ({ + ...prevState, + [adapterLabel] : STATUS.UNDEPLOYING, })); const variables = { @@ -404,43 +448,36 @@ class MeshAdapterConfigComponent extends React.Component { }; changeAdapterState((response, errors) => { - this.props.updateProgress({ showProgress : false }); + updateProgress({ showProgress : false }); if (errors !== undefined) { - console.error(errors) - this.handleError("Unable to Deploy adapter"); - this.setState(prevState => ({ - adapterStates : { - ...prevState.adapterStates, - [adapterLabel] : STATUS.DEPLOYED, - }, - })) + console.error(errors); + handleError("Unable to Deploy adapter"); + setAdapterStates(prevState => ({ + ...prevState, + [adapterLabel] : STATUS.DEPLOYED, + })); } - const notify = this.props.notify; - notify({ message : "Adapter " + response.adapterStatus.toLowerCase(), event_type : EVENT_TYPES.SUCCESS }) - this.setState(prevState => ({ - adapterStates : { - ...prevState.adapterStates, - [adapterLabel] : STATUS.UNDEPLOYED, - }, + + notify({ message : "Adapter " + response.adapterStatus.toLowerCase(), event_type : EVENT_TYPES.SUCCESS }); + + setAdapterStates(prevState => ({ + ...prevState, + [adapterLabel] : STATUS.UNDEPLOYED, })); }, variables); }; - handleError = (msg) => (error) => { - this.props.updateProgress({ showProgress : false }); - const notify = this.props.notify; + const handleError = (msg) => (error) => { + updateProgress({ showProgress : false }); notify({ message : msg, event_type : EVENT_TYPES.ERROR, details : error.toString() }) }; - configureTemplate = () => { - const { classes } = this.props; - const { - availableAdapters, setAdapterURLs, meshAdapters, meshLocationURL, meshLocationURLError, meshDeployURLError, selectedAvailableAdapter, selectedAvailableAdapterError, meshDeployURL, adapterStates - } = this.state; + const configureTemplate = () => { + + let showAdapters = ""; - const self = this; if (meshAdapters.length > 0) { showAdapters = (
    @@ -466,11 +503,11 @@ class MeshAdapterConfigComponent extends React.Component { + } @@ -492,7 +529,7 @@ class MeshAdapterConfigComponent extends React.Component { Undeploy @@ -518,7 +555,7 @@ class MeshAdapterConfigComponent extends React.Component { variant="contained" color="primary" size="large" - onClick={this.handleSubmit} + onClick={handleSubmit} className={classes.button} data-cy="btnSubmitMeshAdapter" > @@ -529,7 +566,7 @@ class MeshAdapterConfigComponent extends React.Component { -
    +
    this.handleDeployPortChange(e.target)} + onChange={(e) => handleDeployPortChange(e.target)} value={meshDeployURL} error={meshDeployURLError} /> @@ -556,7 +593,7 @@ class MeshAdapterConfigComponent extends React.Component { variant="contained" color="primary" size="large" - onClick={this.handleAdapterDeploy} + onClick={handleAdapterDeploy} className={classes.button} > Deploy @@ -569,9 +606,9 @@ class MeshAdapterConfigComponent extends React.Component { ); }; - render() { - return this.configureTemplate(); - } + return ( + configureTemplate() + ) } MeshAdapterConfigComponent.propTypes = { classes : PropTypes.object.isRequired, }; @@ -588,6 +625,4 @@ const mapStateToProps = (state) => { return { meshAdapters, meshAdaptersts }; }; -export default withStyles(styles)( - connect(mapStateToProps, mapDispatchToProps)(withRouter(withNotify(MeshAdapterConfigComponent))) -); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(withNotify(MeshAdapterConfigComponent))); \ No newline at end of file From 6995f8d56b069ab90eaf936a549314ab403b94b3 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Sun, 17 Sep 2023 21:18:12 -0500 Subject: [PATCH 28/44] Remove adapter references in default env vars Signed-off-by: Lee Calcote --- install/docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/docker/docker-compose.yaml b/install/docker/docker-compose.yaml index 77f8c6e1263..87e5838e41f 100644 --- a/install/docker/docker-compose.yaml +++ b/install/docker/docker-compose.yaml @@ -6,7 +6,7 @@ services: - "com.centurylinklabs.watchtower.enable=true" environment: - "PROVIDER_BASE_URLS=https://meshery.layer5.io" - - "ADAPTER_URLS=meshery-istio:10000 meshery-linkerd:10001 meshery-consul:10002 meshery-nsm:10004 meshery-app-mesh:10005 meshery-kuma:10007 meshery-traefik-mesh:10006 meshery-nginx-sm:10010 meshery-cilium:10012" + # - "ADAPTER_URLS=meshery-istio:10000 meshery-linkerd:10001 meshery-consul:10002 meshery-nsm:10004 meshery-app-mesh:10005 meshery-kuma:10007 meshery-traefik-mesh:10006 meshery-nginx-sm:10010 meshery-cilium:10012" - "EVENT=mesheryLocal" - "KUBECONFIG_FOLDER=/home/appuser/.kube" - "PORT=9081" From b1ba6857f729649d4bb1cb468453a0b82cc27bb1 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Sun, 17 Sep 2023 21:21:17 -0500 Subject: [PATCH 29/44] Remove adapters from default configuration Signed-off-by: Lee Calcote --- install/docker-extension/docker-compose.yaml | 45 ++----------------- .../ExtensionComponent/ExtensionComponent.js | 6 +-- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/install/docker-extension/docker-compose.yaml b/install/docker-extension/docker-compose.yaml index 97dfeeb2da0..67524ece6e0 100644 --- a/install/docker-extension/docker-compose.yaml +++ b/install/docker-extension/docker-compose.yaml @@ -10,52 +10,13 @@ services: pull_policy: always environment: - "PROVIDER_BASE_URLS=https://meshery.layer5.io" - - "ADAPTER_URLS=meshery-istio:10000 meshery-linkerd:10001 meshery-consul:10002 meshery-nginx-sm:10010 meshery-app-mesh:10005 meshery-kuma:10007 meshery-traefik-mesh:10006 meshery-cilium:10012" + # - "ADAPTER_URLS=meshery-istio:10000 meshery-linkerd:10001 meshery-consul:10002 meshery-nginx-sm:10010 meshery-app-mesh:10005 meshery-kuma:10007 meshery-traefik-mesh:10006 meshery-cilium:10012" - "EVENT=mesheryLocal" + - "KUBECONFIG_FOLDER=/home/appuser/.kube" - "PORT=9081" volumes: - $HOME/.kube:/home/appuser/.kube:ro - $HOME/.minikube:$HOME/.minikube:ro + - /var/run/docker.sock:/var/run/docker.sock ports: - "9081:9081" - meshery-istio: - image: layer5/meshery-istio:stable-latest - pull_policy: always - ports: - - "10000:10000" - meshery-linkerd: - image: layer5/meshery-linkerd:stable-latest - pull_policy: always - ports: - - "10001:10001" - meshery-consul: - image: layer5/meshery-consul:stable-latest - pull_policy: always - ports: - - "10002:10002" - meshery-app-mesh: - image: layer5/meshery-app-mesh:stable-latest - pull_policy: always - ports: - - "10005:10005" - meshery-traefik-mesh: - image: layer5/meshery-traefik-mesh:stable-latest - pull_policy: always - ports: - - "10006:10006" - meshery-kuma: - image: layer5/meshery-kuma:stable-latest - pull_policy: always - ports: - - "10007:10007" - meshery-nginx-sm: - image: layer5/meshery-nginx-sm:stable-latest - pull_policy: always - ports: - - "10010:10010" - meshery-cilium: - image: layer5/meshery-cilium:stable-latest - pull_policy: always - ports: - - "10012:10012" - diff --git a/install/docker-extension/ui/src/components/ExtensionComponent/ExtensionComponent.js b/install/docker-extension/ui/src/components/ExtensionComponent/ExtensionComponent.js index 873678dce22..34fe848d13f 100644 --- a/install/docker-extension/ui/src/components/ExtensionComponent/ExtensionComponent.js +++ b/install/docker-extension/ui/src/components/ExtensionComponent/ExtensionComponent.js @@ -457,7 +457,7 @@ const ExtensionsComponent = () => { backgroundColor: isDarkTheme ? '#393F49' : '#D7DADE', }} > - {!emptystate ? ( + {/* {!emptystate ? (
    Deploy a Service Mesh @@ -513,9 +513,9 @@ const ExtensionsComponent = () => { meshes
    - )} + )} */} - + {mesheryVersion} From c1c662f6b2279e4014e6a618954a4f825d355346 Mon Sep 17 00:00:00 2001 From: EraKin575 Date: Mon, 18 Sep 2023 10:37:23 +0530 Subject: [PATCH 30/44] removed unncessary comments Signed-off-by: EraKin575 --- ui/components/MeshAdapterConfigComponent.js | 59 +-------------------- 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/ui/components/MeshAdapterConfigComponent.js b/ui/components/MeshAdapterConfigComponent.js index 3468c66deb2..d5d593ae075 100644 --- a/ui/components/MeshAdapterConfigComponent.js +++ b/ui/components/MeshAdapterConfigComponent.js @@ -15,55 +15,6 @@ import { withNotify,useNotification } from "../utils/hooks/useNotification"; import { EVENT_TYPES } from "../lib/event-types"; import BadgeAvatars from './CustomAvatar'; -// const styles = (theme) => ({ -// wrapperClass : { -// padding : theme.spacing(5), -// backgroundColor : theme.palette.secondary.elevatedComponents, -// borderBottomLeftRadius : theme.spacing(1), -// borderBottomRightRadius : theme.spacing(1), -// marginTop : theme.spacing(2), -// }, -// buttons : { -// display : "flex", -// justifyContent : "flex-end", paddingTop : "2rem" -// }, -// button : { -// marginLeft : theme.spacing(1), -// }, -// margin : { margin : theme.spacing(1), }, -// alreadyConfigured : { -// textAlign : "center", -// padding : theme.spacing(20), -// }, -// colorSwitchBase : { -// color : blue[300], -// "&$colorChecked" : { -// color : blue[500], -// "& + $colorBar" : { backgroundColor : blue[500], }, -// }, -// }, -// colorBar : {}, -// colorChecked : {}, -// fileLabel : { width : "100%", }, -// fileLabelText : {}, -// inClusterLabel : { paddingRight : theme.spacing(2), }, -// alignCenter : { textAlign : "center", }, -// alignRight : { -// textAlign : "right", -// marginBottom : theme.spacing(2), -// }, -// fileInputStyle : { opacity : "0.01", }, -// // icon : { width : theme.spacing(2.5), }, -// icon : { -// width : 20, -// height : 20 -// }, -// istioIcon : { width : theme.spacing(1.5), }, -// chip : { -// marginRight : theme.spacing(1), -// marginBottom : theme.spacing(1), -// } -// }); const useStyles = makeStyles((theme) => ({ wrapperClass : { @@ -126,15 +77,7 @@ const STATUS = { const MeshAdapterConfigComponent = (props) => { const labelRef = useRef(null) - // this.state = { - // meshAdapters, - // setAdapterURLs : [], - // availableAdapters : [], - // ts : new Date(), - // meshLocationURLError : false, - // selectedAvailableAdapterError : false, - // adapterStates : {}, - // }; + const [meshAdapters, setMeshAdapters] = useState(props.meshAdapters); const [setAdapterURLs, setSetAdapterURLs] = useState([]); const [availableAdapters, setAvailableAdapters] = useState([]); From 0aade646c445c54072eb28e8f0a0a9142186a01d Mon Sep 17 00:00:00 2001 From: EraKin575 Date: Mon, 18 Sep 2023 13:36:59 +0530 Subject: [PATCH 31/44] removed unnecessary wrapper Signed-off-by: EraKin575 --- ui/components/MeshAdapterConfigComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/components/MeshAdapterConfigComponent.js b/ui/components/MeshAdapterConfigComponent.js index d5d593ae075..a6ebffc6844 100644 --- a/ui/components/MeshAdapterConfigComponent.js +++ b/ui/components/MeshAdapterConfigComponent.js @@ -11,7 +11,7 @@ import ReactSelectWrapper from "./ReactSelectWrapper"; import { updateAdaptersInfo, updateProgress } from "../lib/store"; import dataFetch from "../lib/data-fetch"; import changeAdapterState from './graphql/mutations/AdapterStatusMutation'; -import { withNotify,useNotification } from "../utils/hooks/useNotification"; +import { useNotification } from "../utils/hooks/useNotification"; import { EVENT_TYPES } from "../lib/event-types"; import BadgeAvatars from './CustomAvatar'; @@ -568,4 +568,4 @@ const mapStateToProps = (state) => { return { meshAdapters, meshAdaptersts }; }; -export default connect(mapStateToProps, mapDispatchToProps)(withRouter(withNotify(MeshAdapterConfigComponent))); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(MeshAdapterConfigComponent)); \ No newline at end of file From d7218ae23fd149eabcc7b505f3c40bd11c16b9fc Mon Sep 17 00:00:00 2001 From: yash sharma Date: Mon, 18 Sep 2023 16:21:25 +0530 Subject: [PATCH 32/44] Added Create connection modal in connections page Signed-off-by: yash sharma --- ui/components/connections/index.js | 261 ++++++++++++++++++++--------- 1 file changed, 185 insertions(+), 76 deletions(-) diff --git a/ui/components/connections/index.js b/ui/components/connections/index.js index 75f0fbb2355..65c3ecabe66 100644 --- a/ui/components/connections/index.js +++ b/ui/components/connections/index.js @@ -1,35 +1,30 @@ -import { - NoSsr, - TableCell, - // Button, - Tooltip, - Link, -} from "@material-ui/core"; +import { NoSsr, TableCell, Button, Tooltip, Link } from "@material-ui/core"; import { withStyles } from "@material-ui/core/styles"; // import EditIcon from "@material-ui/icons/Edit"; // import YoutubeSearchedForIcon from '@mui/icons-material/YoutubeSearchedFor'; -import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; import React, { useEffect, useRef, useState } from "react"; import Moment from "react-moment"; import { connect } from "react-redux"; import { bindActionCreators } from "redux"; import { updateProgress } from "../../lib/store"; -import { /* Avatar, */ Chip, /* FormControl, */ } from "@mui/material"; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import AssignmentTurnedInIcon from '@mui/icons-material/AssignmentTurnedIn'; -import ExploreIcon from '@mui/icons-material/Explore'; -import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'; +import { /* Avatar, */ Chip /* FormControl, */ } from "@mui/material"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import AssignmentTurnedInIcon from "@mui/icons-material/AssignmentTurnedIn"; +import ExploreIcon from "@mui/icons-material/Explore"; +import RemoveCircleIcon from "@mui/icons-material/RemoveCircle"; import classNames from "classnames"; // import ReactSelectWrapper from "../ReactSelectWrapper"; import dataFetch from "../../lib/data-fetch"; -import LaunchIcon from '@mui/icons-material/Launch'; -import TableRow from '@mui/material/TableRow'; +import LaunchIcon from "@mui/icons-material/Launch"; +import TableRow from "@mui/material/TableRow"; import { useNotification } from "../../utils/hooks/useNotification"; import { EVENT_TYPES } from "../../lib/event-types"; import CustomColumnVisibilityControl from "../../utils/custom-column"; import SearchBar from "../../utils/custom-search"; import ResponsiveDataTable from "../../utils/data-table"; import useStyles from "../../assets/styles/general/tool.styles"; +import Modal from "../Modal"; const styles = (theme) => ({ grid : { padding : theme.spacing(2) }, @@ -69,8 +64,8 @@ const styles = (theme) => ({ fontWeight : "400", }, "&:hover" : { - boxShadow : "0px 1px 2px 0px rgba(0, 0, 0, 0.25)" - } + boxShadow : "0px 1px 2px 0px rgba(0, 0, 0, 0.25)", + }, }, capitalize : { textTransform : "capitalize", @@ -82,7 +77,7 @@ const styles = (theme) => ({ background : `${theme.palette.secondary.default}30 !important`, "& .MuiSvgIcon-root" : { color : `${theme.palette.secondary.default} !important`, - } + }, }, connected : { "& .MuiChip-label" : { @@ -91,7 +86,7 @@ const styles = (theme) => ({ background : `${theme.palette.secondary.success}30 !important`, "& .MuiSvgIcon-root" : { color : `${theme.palette.secondary.success} !important`, - } + }, }, registered : { "& .MuiChip-label" : { @@ -100,7 +95,7 @@ const styles = (theme) => ({ background : `${theme.palette.secondary.primary}30 !important`, "& .MuiSvgIcon-root" : { color : `${theme.palette.secondary.primary} !important`, - } + }, }, discovered : { "& .MuiChip-label" : { @@ -109,7 +104,7 @@ const styles = (theme) => ({ background : `${theme.palette.secondary.warning}30 !important`, "& .MuiSvgIcon-root" : { color : `${theme.palette.secondary.warning} !important`, - } + }, }, deleted : { "& .MuiChip-label" : { @@ -118,47 +113,142 @@ const styles = (theme) => ({ background : `${theme.palette.secondary.lightError}30 !important`, "& .MuiSvgIcon-root" : { color : `${theme.palette.secondary.error} !important`, - } + }, }, expandedRows : { - background : `${theme.palette.secondary.default}10` - } + background : `${theme.palette.secondary.default}10`, + }, }); const ACTION_TYPES = { FETCH_CONNECTIONS : { name : "FETCH_CONNECTIONS", - error_msg : "Failed to fetch connections" + error_msg : "Failed to fetch connections", }, }; -function Connections({ classes, updateProgress }) { +/** + * Parent Component for Connection Component + * + * @important + * - Keep the component's responsibilities focused on connection management. Avoid adding unrelated functionality and state. +*/ + +function ConnectionManagementPage(props) { + const [createConnectionModal, setCreateConnectionModal] = useState({ + open : false, + }); + const [createConnection, setCreateConnection] = useState({}); + + const handleCreateConnectionModalOpen = () => { + setCreateConnectionModal({ open : true }); + }; + + const handleCreateConnectionModalClose = () => { + setCreateConnectionModal({ open : false }); + }; + + const handleCreateConnectionSubmit = () => {}; + + useEffect(() => { + dataFetch( + "/api/schema/resource/helmRepo", + { + method : "GET", + credentials : "include", + }, + (result) => { + setCreateConnection(result); + } + ); + }, []); + + return ( + <> + + {createConnectionModal.open && ( + } + // submitBtnIcon={} + /> + )} + + ); +} + +function Connections({ + classes, + updateProgress, + onOpenCreateConnectionModal +}) { const [page, setPage] = useState(0); const [count, setCount] = useState(0); const [pageSize, setPageSize] = useState(0); const [connections, setConnections] = useState([]); - const [search,setSearch] = useState(""); - const { notify } = useNotification() + const [search, setSearch] = useState(""); + const { notify } = useNotification(); const StyleClass = useStyles(); const searchTimeout = useRef(null); + const handleCreateConnectionModalOpen = () => { + onOpenCreateConnectionModal(); + }; + const status = (value) => { switch (value) { - case 'ignored': - return } label={value} /> - case 'connected': - return } label={value} /> - case 'REGISTERED': - return } label={value.toLowerCase()} /> - case 'discovered': - return } label={value} /> - case 'deleted': - return } label={value} /> + case "ignored": + return ( + } + label={value} + /> + ); + case "connected": + return ( + } + label={value} + /> + ); + case "REGISTERED": + return ( + } + label={value.toLowerCase()} + /> + ); + case "discovered": + return ( + } label={value} /> + ); + case "deleted": + return ( + } + label={value} + /> + ); default: - return "-" + return "-"; } - } + }; const columns = [ { @@ -174,8 +264,14 @@ function Connections({ classes, updateProgress }) { }, customBodyRender : (value, tableMeta) => { return ( - - + + {value} @@ -183,8 +279,8 @@ function Connections({ classes, updateProgress }) { ); - } - } + }, + }, }, { name : "type", @@ -252,7 +348,16 @@ function Connections({ classes, updateProgress }) { }, customBodyRender : function CustomBody(value) { return ( - {value}} placement="top" arrow interactive > + + {value} + + } + placement="top" + arrow + interactive + > {value} ); @@ -272,7 +377,16 @@ function Connections({ classes, updateProgress }) { }, customBodyRender : function CustomBody(value) { return ( - {value}} placement="top" arrow interactive > + + {value} + + } + placement="top" + arrow + interactive + > {value} ); @@ -291,12 +405,10 @@ function Connections({ classes, updateProgress }) { ); }, customBodyRender : function CustomBody(value) { - return ( - status(value) - ); + return status(value); }, }, - } + }, ]; // const handleChange = () => { @@ -322,9 +434,9 @@ function Connections({ classes, updateProgress }) { text : "connection(s) selected", }, }, - enableNestedDataAccess : '.', + enableNestedDataAccess : ".", onSearchClose : () => { - setSearch("") + setSearch(""); }, onTableChange : (action, tableState) => { switch (action) { @@ -357,30 +469,27 @@ function Connections({ classes, updateProgress }) { renderExpandableRow : (_, tableMeta) => { return ( - - - - Server Build SHA: {connections ? connections[tableMeta.rowIndex]?.metadata?.server_build_sha : '-'} - + - Server Version: {connections ? connections[tableMeta.rowIndex]?.metadata?.server_version : '-'} + Server Build SHA: {connections ? connections[tableMeta.rowIndex]?.metadata?.server_build_sha : "-"} + Server Version: {connections ? connections[tableMeta.rowIndex]?.metadata?.server_version : "-"} + ); }, }; - /** * fetch connections when the page loads */ useEffect(() => { - getConnections(page, pageSize,search) + getConnections(page, pageSize, search); }, [page, pageSize, search]); - const getConnections = (page, pageSize,search) => { + const getConnections = (page, pageSize, search) => { if (!search) search = ""; dataFetch( `/api/integrations/connections?page=${page}&pagesize=${pageSize}&search=${encodeURIComponent(search)}`, @@ -389,26 +498,27 @@ function Connections({ classes, updateProgress }) { method : "GET", }, (res) => { - setConnections(res?.connections) - setPage(res?.page || 0) - setCount(res?.total_count || 0) - setPageSize(res?.page_size || 0) + setConnections(res?.connections); + setPage(res?.page || 0); + setCount(res?.total_count || 0); + setPageSize(res?.page_size || 0); }, handleError(ACTION_TYPES.FETCH_CONNECTIONS) ); - } + }; const handleError = (action) => (error) => { updateProgress({ showProgress : false }); - notify({ message : `${action.error_msg}: ${error}`, event_type : EVENT_TYPES.ERROR, details : error.toString() }) + notify({ message : `${action.error_msg}: ${error}`, event_type : EVENT_TYPES.ERROR, details : error.toString() }); }; + console.log("connection page renders"); const [tableCols, updateCols] = useState(columns); const [columnVisibility, setColumnVisibility] = useState(() => { // Initialize column visibility based on the original columns' visibility const initialVisibility = {}; - columns.forEach(col => { + columns.forEach((col) => { initialVisibility[col.name] = col.options?.display !== false; }); return initialVisibility; @@ -417,21 +527,20 @@ function Connections({ classes, updateProgress }) { return ( <> -
    +
    - {/* */} + Create Connection +
    -
    { }; // @ts-ignore -export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Connections)); +export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(ConnectionManagementPage)); + From 043cf1edf3615fa6b635a83251de5ccc5a67d766 Mon Sep 17 00:00:00 2001 From: yash sharma Date: Mon, 18 Sep 2023 18:17:39 +0530 Subject: [PATCH 33/44] Change button names Signed-off-by: yash sharma --- ui/components/connections/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/components/connections/index.js b/ui/components/connections/index.js index 65c3ecabe66..74dde0efc3a 100644 --- a/ui/components/connections/index.js +++ b/ui/components/connections/index.js @@ -178,9 +178,9 @@ function ConnectionManagementPage(props) { uiSchema={createConnection.uiSchema} handleClose={handleCreateConnectionModalClose} handleSubmit={handleCreateConnectionSubmit} - title="Create Connection" - submitBtnText="Create" - // leftHeaderIcon={} + title="Connect Helm Repository" + submitBtnText="Connect" + // leftHeaderIcon={ } // submitBtnIcon={} /> )} @@ -539,7 +539,7 @@ function Connections({ onClick={handleCreateConnectionModalOpen} style={{ marginRight : "2rem" }} > - Create Connection + Connect Helm Repository
    From 9c4da5b633d07c223ec344f2ea7c3426072cb32f Mon Sep 17 00:00:00 2001 From: yash sharma Date: Mon, 18 Sep 2023 18:17:58 +0530 Subject: [PATCH 34/44] added astrek if field is required Signed-off-by: yash sharma --- .../PatternService/RJSFCustomComponents/CustomBaseInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js b/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js index 5302ce093f1..2239e9ed314 100644 --- a/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js +++ b/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js @@ -13,6 +13,7 @@ const BaseInput = (props) => { const xRjsfGridArea = props.schema?.["x-rjsf-grid-area"]; // check if the field is used in different modal (e.g. publish) const name = (additional ? "Value" : props.label) // || props.id?.split('_')[-1].trim() const focused = props.options?.focused // true for datetime-local + const isRequired = props?.required; const prettifiedName = name || 'Enter a value' const [isFocused, setIsFocused] = React.useState(false); const style = { @@ -35,7 +36,7 @@ const BaseInput = (props) => { <>
    { xRjsfGridArea && - {prettifiedName} + {prettifiedName} } Date: Mon, 18 Sep 2023 20:39:02 +0530 Subject: [PATCH 35/44] replace > with . Signed-off-by: MUzairS15 --- ui/components/DryRun/DryRunComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/DryRun/DryRunComponent.js b/ui/components/DryRun/DryRunComponent.js index f5722b7481a..fd6febb8aa4 100644 --- a/ui/components/DryRun/DryRunComponent.js +++ b/ui/components/DryRun/DryRunComponent.js @@ -89,7 +89,7 @@ function getFieldPathString(fieldPath) { return ""; } - return fieldPath.split(".").splice(2).join(" > "); + return fieldPath.split(".").splice(2).join("."); } // errors - [{type, fieldPath, message}] From c3cf550132c7f68084bd6931bdc16f375f1c5e68 Mon Sep 17 00:00:00 2001 From: MUzairS15 Date: Mon, 18 Sep 2023 23:05:55 +0530 Subject: [PATCH 36/44] bump meshkit Signed-off-by: MUzairS15 --- go.mod | 2 +- go.sum | 4 +- server/handlers/error.go | 2 +- .../handlers/meshery_application_handler.go | 26 ++- server/handlers/meshery_pattern_handler.go | 150 ++++++++++++++---- 5 files changed, 140 insertions(+), 44 deletions(-) diff --git a/go.mod b/go.mod index d6da2e5e483..dd118763fa1 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/jinzhu/copier v0.3.5 github.com/layer5io/gowrk2 v0.6.1 github.com/layer5io/meshery-operator v0.6.10 - github.com/layer5io/meshkit v0.6.65 + github.com/layer5io/meshkit v0.6.66 github.com/layer5io/meshsync v0.6.14 github.com/layer5io/nighthawk-go v1.0.6 github.com/layer5io/service-mesh-performance v0.6.1 diff --git a/go.sum b/go.sum index ea10c1151f2..ab5f6890af9 100644 --- a/go.sum +++ b/go.sum @@ -1571,8 +1571,8 @@ github.com/layer5io/gowrk2 v0.6.1 h1:0eBj7VFYJ+QTMJt7i3PzxDJTEL+X+zWx4OP+lARWIoI github.com/layer5io/gowrk2 v0.6.1/go.mod h1:ugxQ23+HwQ8dmZYJd1LScw/TLKbdgfN6OOtg6iYMljg= github.com/layer5io/meshery-operator v0.6.10 h1:4YiznhS4AO/bA+uHBxCYp9Fc9w9LU2sopE3oJBdRU/Y= github.com/layer5io/meshery-operator v0.6.10/go.mod h1:RX9yjSvJS0KAdWOb/zRfYU/mSOVP1ySuUUlxHhrms1M= -github.com/layer5io/meshkit v0.6.65 h1:JltJ5hq8z/JIJf8V0m6UrsZ/PvIeKevLeIuAvH8q+DU= -github.com/layer5io/meshkit v0.6.65/go.mod h1:ZepHoPUmrDQK6T4ARmyWfKy8HejxFdJsoqC1cq4Slb8= +github.com/layer5io/meshkit v0.6.66 h1:oJKgab+7nlp2EArTwq9r0ZW+/+UIPrY5Efs4eLqIpdg= +github.com/layer5io/meshkit v0.6.66/go.mod h1:/EX5QLmgZpLPqhBHGvNqyHM6ljfc6hYwULCEcnqTCVw= github.com/layer5io/meshsync v0.6.14 h1:y5Fbq76WGYWjdzYFNOD1YgowP/0m/NxPZ/hA1JGJ3RA= github.com/layer5io/meshsync v0.6.14/go.mod h1:21VTdYITKXpBSb+kj2CcR3T0KbrcLJwhiewQRKTqvUM= github.com/layer5io/nighthawk-go v1.0.6 h1:YMCw65FvwpbByX+M7McdNYRNDW9oOw3GQaXJ1RMDdGw= diff --git a/server/handlers/error.go b/server/handlers/error.go index 65e17e46518..a40d14d815c 100644 --- a/server/handlers/error.go +++ b/server/handlers/error.go @@ -464,7 +464,7 @@ func ErrDecodePattern(err error) error { } func ErrParsePattern(err error) error { - return errors.New(ErrParsePatternCode, errors.Alert, []string{"Error failed to parse pattern file"}, []string{err.Error()}, []string{}, []string{}) + return errors.New(ErrParsePatternCode, errors.Alert, []string{"Error failed to parse pattern file from cytoJSON format"}, []string{err.Error()}, []string{}, []string{}) } func ErrConvertPattern(err error) error { diff --git a/server/handlers/meshery_application_handler.go b/server/handlers/meshery_application_handler.go index aa6a7850d6b..eeec29f1121 100644 --- a/server/handlers/meshery_application_handler.go +++ b/server/handlers/meshery_application_handler.go @@ -135,10 +135,10 @@ func (h *Handler) handleApplicationPOST( } var parsedBody *MesheryApplicationRequestBody if err := json.NewDecoder(r.Body).Decode(&parsedBody); err != nil { - http.Error(rw, ErrRetrieveData(err).Error(), http.StatusBadRequest) + http.Error(rw, ErrRequestBody(err).Error(), http.StatusBadRequest) addMeshkitErr(&res, ErrRetrieveData(err)) event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ - "error": ErrRetrieveData(err), + "error": ErrRequestBody(err), }).WithDescription("Unable to parse uploaded application.").Build() _ = provider.PersistEvent(event) @@ -491,7 +491,9 @@ func (h *Handler) handleApplicationPOST( h.formatApplicationOutput(rw, resp, format, &res, eventBuilder) eventBuilder.WithSeverity(events.Informational) - _ = provider.PersistEvent(eventBuilder.Build()) + event := eventBuilder.Build() + go h.config.EventBroadcaster.Publish(userID, event) + _ = provider.PersistEvent(event) var mesheryApplicationContent []models.MesheryApplication err = json.Unmarshal(resp, &mesheryApplicationContent) @@ -537,6 +539,10 @@ func (h *Handler) handleApplicationPOST( } h.formatApplicationOutput(rw, byt, format, &res, eventBuilder) + + event := eventBuilder.Build() + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) } func (h *Handler) handleApplicationUpdate(rw http.ResponseWriter, @@ -674,7 +680,9 @@ func (h *Handler) handleApplicationUpdate(rw http.ResponseWriter, go h.config.ConfigurationChannel.PublishApplications() h.formatApplicationOutput(rw, resp, format, &res, eventBuilder) - _ = provider.PersistEvent(eventBuilder.Build()) + event := eventBuilder.Build() + go h.config.EventBroadcaster.Publish(userID, event) + _ = provider.PersistEvent(event) return } @@ -718,7 +726,10 @@ func (h *Handler) handleApplicationUpdate(rw http.ResponseWriter, eventBuilder.WithSeverity(events.Informational) h.formatApplicationOutput(rw, resp, format, &res, eventBuilder) - _ = provider.PersistEvent(eventBuilder.Build()) + event := eventBuilder.Build() + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) + } // swagger:route GET /api/application ApplicationsAPI idGetMesheryApplications @@ -789,6 +800,9 @@ func (h *Handler) DeleteMesheryApplicationHandler( eventBuilder := events.NewEvent().FromUser(userID).FromSystem(*h.SystemID).WithCategory("application").WithAction("delete").ActedUpon(uuid.FromStringOrNil(applicationID)) resp, err := provider.DeleteMesheryApplication(r, applicationID) + mesheryApplication := models.MesheryApplication{} + _ = json.Unmarshal(resp, &mesheryApplication) + if err != nil { errAppDelete := ErrDeleteApplication(err) h.log.Error(errAppDelete) @@ -801,7 +815,7 @@ func (h *Handler) DeleteMesheryApplicationHandler( return } - event := eventBuilder.WithSeverity(events.Informational).WithDescription("Application deleted.").Build() + event := eventBuilder.WithSeverity(events.Informational).WithDescription(fmt.Sprintf("Application %s deleted.", mesheryApplication.Name)).Build() _ = provider.PersistEvent(event) go h.config.EventBroadcaster.Publish(userID, event) diff --git a/server/handlers/meshery_pattern_handler.go b/server/handlers/meshery_pattern_handler.go index 9286f64abe1..08f8864c015 100644 --- a/server/handlers/meshery_pattern_handler.go +++ b/server/handlers/meshery_pattern_handler.go @@ -7,11 +7,12 @@ import ( "net/http" "strings" - "github.com/google/uuid" + "github.com/gofrs/uuid" "github.com/gorilla/mux" "github.com/layer5io/meshery/server/meshes" "github.com/layer5io/meshery/server/models" "github.com/layer5io/meshkit/errors" + "github.com/layer5io/meshkit/models/events" pCore "github.com/layer5io/meshery/server/models/pattern/core" "github.com/layer5io/meshery/server/models/pattern/stages" @@ -60,7 +61,7 @@ func (h *Handler) handlePatternPOST( rw http.ResponseWriter, r *http.Request, _ *models.Preference, - _ *models.User, + user *models.User, provider models.Provider, ) { defer func() { @@ -68,27 +69,46 @@ func (h *Handler) handlePatternPOST( }() var err error + userID := uuid.FromStringOrNil(user.ID) + eventBuilder := events.NewEvent().FromUser(userID).FromSystem(*h.SystemID).WithCategory("pattern").WithAction("create").ActedUpon(userID) + res := meshes.EventsResponse{ Component: "core", ComponentName: "Design", - OperationId: uuid.NewString(), + OperationId: uuid.Nil.String(), // to be removed EventType: meshes.EventType_INFO, } var parsedBody *MesheryPatternRequestBody if err := json.NewDecoder(r.Body).Decode(&parsedBody); err != nil { h.log.Error(ErrRequestBody(err)) http.Error(rw, ErrRequestBody(err).Error(), http.StatusBadRequest) - addMeshkitErr(&res, ErrRequestBody(err)) - go h.EventsBuffer.Publish(&res) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrRequestBody(err), + }).WithDescription("Unable to parse uploaded pattern.").Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } + actedUpon := &userID + if parsedBody.PatternData != nil && parsedBody.PatternData.ID != nil { + actedUpon = parsedBody.PatternData.ID + } + + eventBuilder.ActedUpon(*actedUpon) + token, err := provider.GetProviderToken(r) if err != nil { h.log.Error(ErrRetrieveUserToken(err)) http.Error(rw, ErrRetrieveUserToken(err).Error(), http.StatusInternalServerError) - addMeshkitErr(&res, ErrRequestBody(err)) - go h.EventsBuffer.Publish(&res) + event := eventBuilder.WithSeverity(events.Critical).WithMetadata(map[string]interface{}{ + "error": ErrRetrieveUserToken(err), + }).WithDescription("No auth token provided in the request.").Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) + return } @@ -99,8 +119,12 @@ func (h *Handler) handlePatternPOST( if err != nil { rw.WriteHeader(http.StatusBadRequest) fmt.Fprintf(rw, "%s", err) - addMeshkitErr(&res, ErrSavePattern(err)) - go h.EventsBuffer.Publish(&res) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrSavePattern(err), + }).WithDescription("Pattern save failed, cytoJSON could be malformed.").Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } @@ -108,17 +132,25 @@ func (h *Handler) handlePatternPOST( if err != nil { rw.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(rw, "%s", err) - addMeshkitErr(&res, ErrSavePattern(err)) - go h.EventsBuffer.Publish(&res) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrSavePattern(err), + }).WithDescription(ErrSavePattern(err).Error()).Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } patternName, err := models.GetPatternName(string(pfByt)) if err != nil { - h.log.Error(ErrGetPattern(err)) - http.Error(rw, ErrGetPattern(err).Error(), http.StatusBadRequest) - addMeshkitErr(&res, ErrGetPattern(err)) - go h.EventsBuffer.Publish(&res) + h.log.Error(ErrSavePattern(err)) + http.Error(rw, ErrSavePattern(err).Error(), http.StatusBadRequest) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrSavePattern(err), + }).WithDescription("unable to get \"name\" from the pattern.").Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } @@ -139,13 +171,17 @@ func (h *Handler) handlePatternPOST( if err != nil { h.log.Error(ErrSavePattern(err)) http.Error(rw, ErrSavePattern(err).Error(), http.StatusInternalServerError) - addMeshkitErr(&res, ErrSavePattern(err)) - go h.EventsBuffer.Publish(&res) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrSavePattern(err), + }).WithDescription(ErrSavePattern(err).Error()).Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } go h.config.ConfigurationChannel.PublishPatterns() - h.formatPatternOutput(rw, resp, format, &res) + h.formatPatternOutput(rw, resp, format, &res, eventBuilder) return } @@ -158,7 +194,8 @@ func (h *Handler) handlePatternPOST( return } - h.formatPatternOutput(rw, byt, format, &res) + h.formatPatternOutput(rw, byt, format, &res, eventBuilder) + return } // If Content is not empty then assume it's a local upload @@ -178,8 +215,12 @@ func (h *Handler) handlePatternPOST( if err != nil { h.log.Error(ErrSavePattern(err)) http.Error(rw, ErrSavePattern(err).Error(), http.StatusBadRequest) - addMeshkitErr(&res, ErrSavePattern(err)) - go h.EventsBuffer.Publish(&res) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrSavePattern(err), + }).WithDescription("unable to get \"name\" from the pattern.").Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } parsedBody.PatternData.Name = patternName @@ -202,12 +243,20 @@ func (h *Handler) handlePatternPOST( if err != nil { h.log.Error(ErrSavePattern(err)) http.Error(rw, ErrSavePattern(err).Error(), http.StatusInternalServerError) - addMeshkitErr(&res, ErrSavePattern(err)) - go h.EventsBuffer.Publish(&res) + + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrSavePattern(err), + }).WithDescription(ErrSavePattern(err).Error()).Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } + h.formatPatternOutput(rw, resp, format, &res, eventBuilder) + event := eventBuilder.Build() + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) go h.config.ConfigurationChannel.PublishPatterns() - h.formatPatternOutput(rw, resp, format, &res) return } @@ -220,7 +269,10 @@ func (h *Handler) handlePatternPOST( return } - h.formatPatternOutput(rw, byt, format, &res) + h.formatPatternOutput(rw, byt, format, &res, eventBuilder) + event := eventBuilder.Build() + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } @@ -230,12 +282,19 @@ func (h *Handler) handlePatternPOST( if err != nil { h.log.Error(ErrImportPattern(err)) http.Error(rw, ErrImportPattern(err).Error(), http.StatusInternalServerError) - addMeshkitErr(&res, ErrImportPattern(err)) - go h.EventsBuffer.Publish(&res) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrImportPattern(err), + }).WithDescription(ErrImportPattern(err).Error()).Build() + + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } - h.formatPatternOutput(rw, resp, format, &res) + h.formatPatternOutput(rw, resp, format, &res, eventBuilder) + event := eventBuilder.Build() + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } //Depracated: The below logic was used when applications were stored as k8s_manifests. @@ -399,17 +458,35 @@ func (h *Handler) DeleteMesheryPatternHandler( rw http.ResponseWriter, r *http.Request, _ *models.Preference, - _ *models.User, + user *models.User, provider models.Provider, ) { patternID := mux.Vars(r)["id"] + userID := uuid.FromStringOrNil(user.ID) + eventBuilder := events.NewEvent().FromUser(userID).FromSystem(*h.SystemID).WithCategory("pattern").WithAction("delete").ActedUpon(uuid.FromStringOrNil(patternID)) + + mesheryPattern := models.MesheryPattern{} + resp, err := provider.DeleteMesheryPattern(r, patternID) if err != nil { - h.log.Error(ErrDeletePattern(err)) - http.Error(rw, ErrDeletePattern(err).Error(), http.StatusInternalServerError) + errPatternDelete := ErrDeletePattern(err) + + h.log.Error(errPatternDelete) + http.Error(rw, errPatternDelete.Error(), http.StatusInternalServerError) + event := eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": errPatternDelete, + }).WithDescription("Error deleting pattern.").Build() + http.Error(rw, errPatternDelete.Error(), http.StatusInternalServerError) + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) return } + _ = json.Unmarshal(resp, &mesheryPattern) + + event := eventBuilder.WithSeverity(events.Informational).WithDescription(fmt.Sprintf("Pattern %s deleted.", mesheryPattern.Name)).Build() + _ = provider.PersistEvent(event) + go h.config.EventBroadcaster.Publish(userID, event) go h.config.ConfigurationChannel.PublishPatterns() rw.Header().Set("Content-Type", "application/json") fmt.Fprint(rw, string(resp)) @@ -633,7 +710,7 @@ func (h *Handler) GetMesheryPatternHandler( fmt.Fprint(rw, string(resp)) } -func (h *Handler) formatPatternOutput(rw http.ResponseWriter, content []byte, format string, res *meshes.EventsResponse) { +func (h *Handler) formatPatternOutput(rw http.ResponseWriter, content []byte, format string, res *meshes.EventsResponse, eventBuilder *events.EventBuilder) { contentMesheryPatternSlice := make([]models.MesheryPattern, 0) if err := json.Unmarshal(content, &contentMesheryPatternSlice); err != nil { @@ -646,12 +723,15 @@ func (h *Handler) formatPatternOutput(rw http.ResponseWriter, content []byte, fo result := []models.MesheryPattern{} names := []string{} for _, content := range contentMesheryPatternSlice { + eventBuilder.ActedUpon(*content.ID) if format == "cytoscape" { patternFile, err := pCore.NewPatternFile([]byte(content.PatternFile)) if err != nil { http.Error(rw, ErrParsePattern(err).Error(), http.StatusBadRequest) - addMeshkitErr(res, ErrParsePattern(err)) - go h.EventsBuffer.Publish(res) + + eventBuilder.WithSeverity(events.Error).WithMetadata(map[string]interface{}{ + "error": ErrParsePattern(err), + }).WithDescription("Unable to parse pattern file, pattern could be malformed.").Build() return } @@ -681,9 +761,11 @@ func (h *Handler) formatPatternOutput(rw http.ResponseWriter, content []byte, fo obj := "pattern file" http.Error(rw, models.ErrMarshal(err, obj).Error(), http.StatusInternalServerError) addMeshkitErr(res, models.ErrMarshal(err, obj)) + go h.EventsBuffer.Publish(res) return } + eventBuilder.WithDescription(fmt.Sprintf("Design %s saved", strings.Join(names, ","))) rw.Header().Set("Content-Type", "application/json") fmt.Fprint(rw, string(data)) res.Details = "\"" + strings.Join(names, ",") + "\" design saved" From cdd1b5f2ce403f66b5da0911802ddea3aa6b8cfa Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Mon, 18 Sep 2023 15:31:57 -0500 Subject: [PATCH 37/44] Update MAINTAINERS.md Signed-off-by: Lee Calcote --- MAINTAINERS.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 5d8e1c03181..3c730b7be4f 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -4,6 +4,8 @@ | ----------------------- | ----------------- | ----------- | | Hussaina Begum | hexxdump | VMware | | Aadhitya Amarendiran | alphaX86 | Citi | +| Jerod Culpepper | cpepper96 | SAIC | +| Antonette Caldwell | acald-creator | Acquia | ### UI Maintainers @@ -18,19 +20,21 @@ | Name | GitHub | Affiliation | | ------------------- | ------------- | ----------- | -| Aisuko Li | aisuko | Layer5 | +| Aisuko Li | aisuko | RMIT | | Dhiraj Gedam | dheerajng | Citrix | | Haim Helman | thehh1974 | VMware | | Hussaina Begum | hexxdump | VMware | | Ashish Tiwari | revolyssup | API7 | | Michael Gfeller | mgfeller | Computas AS | | Antonette Caldwell | acald-creator | Acquia | +| Xin Huang | gyohuangxin | Intel | ### CI / Build & Release Maintainers | Name | GitHub | Affiliation | | --------------------- | ------------------ | ----------- | | Ashish Tiwari | revolyssup | API7 | +| Pranav Singh | theBeginner86 | Layer5 | | Mario Arriaga | MarioArriaga92 | F5 | ### Docs Maintainers @@ -43,6 +47,8 @@ ### Site Maintainers -| Name | GitHub | Affiliation | -| ---------------------- | ----------- | ----------- | -| Nikhil Ladha. | Nikhil-Ladha | Red Hat | +| Name | GitHub | Affiliation | +| ---------------------- | ----------- | -------------- | +| Nikhil Ladha | Nikhil-Ladha | Red Hat | +| Aaditya Narayan Subedy | asubedy | Fast Retailing | + From 2cc080e89404fb01246feec680c33bf695e70a2f Mon Sep 17 00:00:00 2001 From: Pranav Singh Date: Tue, 19 Sep 2023 02:37:53 +0530 Subject: [PATCH 38/44] fix lint Signed-off-by: Pranav Singh --- ui/rtk-query/index.js | 6 +- ui/store/index.js | 8 +- ui/themes/app.js | 690 +++++++++++++++--------------- ui/utils/hooks/useNotification.js | 4 +- 4 files changed, 354 insertions(+), 354 deletions(-) diff --git a/ui/rtk-query/index.js b/ui/rtk-query/index.js index 9be1c74e7bc..64474db76e9 100644 --- a/ui/rtk-query/index.js +++ b/ui/rtk-query/index.js @@ -1,8 +1,8 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export const api = createApi({ - reducerPath: 'mesheryApi', - baseQuery: fetchBaseQuery({ baseUrl: '/api/' }), - endpoints: () => ({ + reducerPath : 'mesheryApi', + baseQuery : fetchBaseQuery({ baseUrl : '/api/' }), + endpoints : () => ({ }), }) \ No newline at end of file diff --git a/ui/store/index.js b/ui/store/index.js index 823010d91f5..35853945ede 100644 --- a/ui/store/index.js +++ b/ui/store/index.js @@ -3,9 +3,9 @@ import { configureStore } from '@reduxjs/toolkit' import { api } from "../rtk-query/index" export const store = configureStore({ - reducer: { - events: eventsReducer, - [api.reducerPath]: api.reducer, + reducer : { + events : eventsReducer, + [api.reducerPath] : api.reducer, }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), + middleware : (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), }) \ No newline at end of file diff --git a/ui/themes/app.js b/ui/themes/app.js index 570070c9e47..ab9d8bef7f3 100644 --- a/ui/themes/app.js +++ b/ui/themes/app.js @@ -5,200 +5,200 @@ import { iconMedium } from '../css/icons.styles'; const drawerWidth = 256; export const Colors = { - darkJungleGreen: "#1E2117", - caribbeanGreen: "#00D3a9", - keppelGreen: "#00B39F", - charcoal: "#3C494F", + darkJungleGreen : "#1E2117", + caribbeanGreen : "#00D3a9", + keppelGreen : "#00B39F", + charcoal : "#3C494F", } export var darkTheme = createTheme({ - typography: { - useNextVariants: true, - fontFamily: ['Qanelas Soft', 'Roboto', 'Helvectica', 'Arial', 'sans-serif'].join(','), - h5: { - fontWeight: 'bolder', - fontSize: 26, - color: '#FFF', - letterSpacing: 0.5, + typography : { + useNextVariants : true, + fontFamily : ['Qanelas Soft', 'Roboto', 'Helvectica', 'Arial', 'sans-serif'].join(','), + h5 : { + fontWeight : 'bolder', + fontSize : 26, + color : '#FFF', + letterSpacing : 0.5, }, - p: { - color: '#FFF', + p : { + color : '#FFF', }, - h6: { - color: '#FFF', + h6 : { + color : '#FFF', } }, - palette: { - type: "dark", - primary: blueGrey, - secondary: { - main: '#EE5351', - primeColor: '#303030', - dark: '#121212', - titleText: '#FBFBFB', - text: '#FFF', - text2: '#7494a1', - text3: '#FFF', - textMain: "#F6F8F8", - titleBackground: "#000", - mainBackground: '#202020', - mainBackground2: '#303030', - elevatedComponents: '#202020', - elevatedComponents2: '#303030', - elevatedComponents3: '#303030', - lightText: 'rgba(255, 255, 255, 0.54)', - icon: 'rgba(255, 255, 255, 0.54)', - icon2: '#E6E6E6', - iconMain: '#F6F8F8', - disabledIcon: 'rgba(255, 255, 255, 0.26)', - chevron: 'rgb(255, 255, 255, 0.2)', - link: 'rgba(255, 255, 255, 0.7)', - link2: "#05FFCD", - headerColor: '#202020', - sideBar: '#1A1A1A', - drawer: '#252E31', - drawerHover: '#202020', - img: 'invert(0.8)', - appBar: '#363636', - number: '#eee', - completeInvert: 'invert(1)', - canvas: '#1A1A1A', - brightness: 'brightness(200%)', - switcherButtons: '#1e1e1e', - honeyComb: '#303030', - filterChipBackground: '#222222', - searchBackground: "#294957", - searchBorder: "#396679", - tabs: '#202020', - modalTabs: '#363636', - tabHover: '#212121', - confirmationModal: '#111111', - focused: '#00b39f', - primaryModalText: '#FFF', - default: "#9FAFB6", - success: "#00D3A9", - primary: "#86B2C6", - warning: "#EBC017", - error: "#F91313", - lightError: "#B32700", - penColorPrimary: '#E6E6E6', - penColorSecondary: '#E6E6E6', - toolbarBg2: "#464646", + palette : { + type : "dark", + primary : blueGrey, + secondary : { + main : '#EE5351', + primeColor : '#303030', + dark : '#121212', + titleText : '#FBFBFB', + text : '#FFF', + text2 : '#7494a1', + text3 : '#FFF', + textMain : "#F6F8F8", + titleBackground : "#000", + mainBackground : '#202020', + mainBackground2 : '#303030', + elevatedComponents : '#202020', + elevatedComponents2 : '#303030', + elevatedComponents3 : '#303030', + lightText : 'rgba(255, 255, 255, 0.54)', + icon : 'rgba(255, 255, 255, 0.54)', + icon2 : '#E6E6E6', + iconMain : '#F6F8F8', + disabledIcon : 'rgba(255, 255, 255, 0.26)', + chevron : 'rgb(255, 255, 255, 0.2)', + link : 'rgba(255, 255, 255, 0.7)', + link2 : "#05FFCD", + headerColor : '#202020', + sideBar : '#1A1A1A', + drawer : '#252E31', + drawerHover : '#202020', + img : 'invert(0.8)', + appBar : '#363636', + number : '#eee', + completeInvert : 'invert(1)', + canvas : '#1A1A1A', + brightness : 'brightness(200%)', + switcherButtons : '#1e1e1e', + honeyComb : '#303030', + filterChipBackground : '#222222', + searchBackground : "#294957", + searchBorder : "#396679", + tabs : '#202020', + modalTabs : '#363636', + tabHover : '#212121', + confirmationModal : '#111111', + focused : '#00b39f', + primaryModalText : '#FFF', + default : "#9FAFB6", + success : "#00D3A9", + primary : "#86B2C6", + warning : "#EBC017", + error : "#F91313", + lightError : "#B32700", + penColorPrimary : '#E6E6E6', + penColorSecondary : '#E6E6E6', + toolbarBg2 : "#464646", }, }, - p: { - color: '#FFF', + p : { + color : '#FFF', }, - shape: { borderRadius: 8, }, - breakpoints: { - values: { - xs: 0, - sm: 600, - md: 960, - lg: 1280, - xl: 1920, + shape : { borderRadius : 8, }, + breakpoints : { + values : { + xs : 0, + sm : 600, + md : 960, + lg : 1280, + xl : 1920, }, }, }); darkTheme = { ...darkTheme, - overrides: { - MuiSvgIcon: { - root: { + overrides : { + MuiSvgIcon : { + root : { ...iconMedium } }, - MuiOutlinedInput: { - root: { - "&:hover $notchedOutline": { - borderColor: "#00B39F", + MuiOutlinedInput : { + root : { + "&:hover $notchedOutline" : { + borderColor : "#00B39F", }, - "&$focused $notchedOutline": { - borderColor: "#00B39F", + "&$focused $notchedOutline" : { + borderColor : "#00B39F", }, }, }, - MuiCheckbox: { - colorPrimary: { - "&$checked": { - color: "rgba(255, 255, 255, 0.7)", + MuiCheckbox : { + colorPrimary : { + "&$checked" : { + color : "rgba(255, 255, 255, 0.7)", } }, }, - MuiDrawer: { paper: { backgroundColor: '#263238', }, }, - MuiFormLabel: { - root: { - "&$focused": { - color: "#00B39F", + MuiDrawer : { paper : { backgroundColor : '#263238', }, }, + MuiFormLabel : { + root : { + "&$focused" : { + color : "#00B39F", }, } }, - MuiButton: { - label: { textTransform: 'initial', }, - contained: { - boxShadow: 'none', - color: "rgba(255, 255, 255, 0.87)", - backgroundColor: "#3C494F", - '&:hover': { backgroundColor: "#505b61", }, - '&:active': { boxShadow: 'none', }, + MuiButton : { + label : { textTransform : 'initial', }, + contained : { + boxShadow : 'none', + color : "rgba(255, 255, 255, 0.87)", + backgroundColor : "#3C494F", + '&:hover' : { backgroundColor : "#505b61", }, + '&:active' : { boxShadow : 'none', }, }, - containedPrimary: { - backgroundColor: "#00B39F", - '&:hover': { backgroundColor: "#00D3A9", }, + containedPrimary : { + backgroundColor : "#00B39F", + '&:hover' : { backgroundColor : "#00D3A9", }, }, }, - MuiToggleButton: { - label: { - textTransform: 'initial', - color: '#00B39F', + MuiToggleButton : { + label : { + textTransform : 'initial', + color : '#00B39F', }, }, - MuiTabs: { - root: { marginLeft: darkTheme.spacing(1), }, - indicator: { - height: 3, - backgroundColor: "#00B39F", - borderTopLeftRadius: 3, - borderTopRightRadius: 3, + MuiTabs : { + root : { marginLeft : darkTheme.spacing(1), }, + indicator : { + height : 3, + backgroundColor : "#00B39F", + borderTopLeftRadius : 3, + borderTopRightRadius : 3, }, }, - MuiTab: { - root: { - textTransform: 'initial', - margin: '0 16px', - minWidth: 0, + MuiTab : { + root : { + textTransform : 'initial', + margin : '0 16px', + minWidth : 0, // [darkTheme.breakpoints.up('md')]: { // minWidth: 0, // }, }, - labelContainer: { - padding: 0, + labelContainer : { + padding : 0, // [darkTheme.breakpoints.up('md')]: { // padding: 0, // }, }, - selected: { - color: "#00B39F !important" + selected : { + color : "#00B39F !important" } }, - MuiPaper: { root: { backgroundColor: '#363636' }, elevation2: { boxShadow: "0px 4px 0px -2px rgb(0 179 159 / 10%), 0px 4px 0px 0px rgb(0 179 159 / 10%), 0px 2px 0px 0px rgb(0 179 159 / 20%)" } }, - MuiIconButton: { root: { padding: darkTheme.spacing(1), }, colorPrimary: { color: "#FFF" }, }, - MuiTooltip: { tooltip: { borderRadius: 4, }, }, - MuiDivider: { root: { backgroundColor: '#404854', }, }, - MuiListItemText: { primary: { fontWeight: darkTheme.typography.fontWeightMedium, }, }, - MuiListItemIcon: { - root: { - color: 'inherit', - marginRight: 0, - '& svg': { fontSize: 20, }, - justifyContent: 'center', - minWidth: 0 + MuiPaper : { root : { backgroundColor : '#363636' }, elevation2 : { boxShadow : "0px 4px 0px -2px rgb(0 179 159 / 10%), 0px 4px 0px 0px rgb(0 179 159 / 10%), 0px 2px 0px 0px rgb(0 179 159 / 20%)" } }, + MuiIconButton : { root : { padding : darkTheme.spacing(1), }, colorPrimary : { color : "#FFF" }, }, + MuiTooltip : { tooltip : { borderRadius : 4, }, }, + MuiDivider : { root : { backgroundColor : '#404854', }, }, + MuiListItemText : { primary : { fontWeight : darkTheme.typography.fontWeightMedium, }, }, + MuiListItemIcon : { + root : { + color : 'inherit', + marginRight : 0, + '& svg' : { fontSize : 20, }, + justifyContent : 'center', + minWidth : 0 }, }, - MuiAvatar: { - root: { - width: 32, - height: 32, + MuiAvatar : { + root : { + width : 32, + height : 32, }, }, // Global scrollbar and body styles @@ -237,167 +237,167 @@ darkTheme = { // }, // }, }, - props: { MuiTab: { disableRipple: true, }, }, - mixins: { ...darkTheme.mixins, }, + props : { MuiTab : { disableRipple : true, }, }, + mixins : { ...darkTheme.mixins, }, }; let theme = createTheme({ - typography: { + typography : { - fontFamily: ['Qanelas Soft', 'Roboto', 'Helvectica', 'Arial', 'sans-serif'].join(','), - useNextVariants: true, - h5: { - fontWeight: 'bolder', - fontSize: 26, - letterSpacing: 0.5, + fontFamily : ['Qanelas Soft', 'Roboto', 'Helvectica', 'Arial', 'sans-serif'].join(','), + useNextVariants : true, + h5 : { + fontWeight : 'bolder', + fontSize : 26, + letterSpacing : 0.5, }, }, - palette: { - type: "light", + palette : { + type : "light", // primary: { // light: '#cfd8dc', // main: '#607d8b', // dark: '#455a64', // }, - primary: blueGrey, - secondary: { - main: '#EE5351', - primeColor: '#ebeff1', - dark: '#455a64', - titleText: '#7494A1', - text: '#000', - text2: 'rgba(57, 102, 121, .9)', - text3: '#333333', - textMain: "#3C494F", - titleBackground: "rgba(57, 102, 121, .1)", - mainBackground: '#396679', - mainBackground2: '#FFF', - elevatedComponents: '#FFF', - elevatedComponents2: "#eaeff1", - elevatedComponents3: '#FFF', - lightText: 'rgba(0, 0, 0, 0.54)', - icon: 'rgba(0, 0, 0, 0.54)', - icon2: 'gray', - iconMain: '#3C494F', - disabledIcon: 'rgba(0, 0, 0, 0.26)', - chevron: '#FFF', - link: '#000', - link2: "#00b39F", - headerColor: '#eeeeee', - sideBar: '#FFF', - drawer: '#FFF', - drawerHover: '#f2f5f7', - img: 'none', - appBar: '#FFF', - number: '#607d8b', - completeInvert: 'none', - canvas: '#fff', - brightness: 'none', - switcherButtons: '#335c6d', - honeyComb: '#F0F0F0', - filterChipBackground: '#CCCCCC', - searchBackground: "#fff", - searchBorder: "#CCCCCC", - tabs: '#eeeeee87', - modalTabs: '#dddddd', - tabHover: '#e3e3e3', - confirmationModal: 'rgb(234, 235, 236)', - focused: '#607d8b', - primaryModalText: '#FFF', - default: "#51636B", - success: "#00B39F", - primary: "#477E96", - warning: "#F0A303", - error: "#8F1F00", - lightError: "#8F1F00", - penColorPrimary: '#3C494F', - penColorSecondary: '#677E88', - toolbarBg1: "#FFFFFF", + primary : blueGrey, + secondary : { + main : '#EE5351', + primeColor : '#ebeff1', + dark : '#455a64', + titleText : '#7494A1', + text : '#000', + text2 : 'rgba(57, 102, 121, .9)', + text3 : '#333333', + textMain : "#3C494F", + titleBackground : "rgba(57, 102, 121, .1)", + mainBackground : '#396679', + mainBackground2 : '#FFF', + elevatedComponents : '#FFF', + elevatedComponents2 : "#eaeff1", + elevatedComponents3 : '#FFF', + lightText : 'rgba(0, 0, 0, 0.54)', + icon : 'rgba(0, 0, 0, 0.54)', + icon2 : 'gray', + iconMain : '#3C494F', + disabledIcon : 'rgba(0, 0, 0, 0.26)', + chevron : '#FFF', + link : '#000', + link2 : "#00b39F", + headerColor : '#eeeeee', + sideBar : '#FFF', + drawer : '#FFF', + drawerHover : '#f2f5f7', + img : 'none', + appBar : '#FFF', + number : '#607d8b', + completeInvert : 'none', + canvas : '#fff', + brightness : 'none', + switcherButtons : '#335c6d', + honeyComb : '#F0F0F0', + filterChipBackground : '#CCCCCC', + searchBackground : "#fff", + searchBorder : "#CCCCCC", + tabs : '#eeeeee87', + modalTabs : '#dddddd', + tabHover : '#e3e3e3', + confirmationModal : 'rgb(234, 235, 236)', + focused : '#607d8b', + primaryModalText : '#FFF', + default : "#51636B", + success : "#00B39F", + primary : "#477E96", + warning : "#F0A303", + error : "#8F1F00", + lightError : "#8F1F00", + penColorPrimary : '#3C494F', + penColorSecondary : '#677E88', + toolbarBg1 : "#FFFFFF", }, }, - shape: { borderRadius: 8, }, - breakpoints: { - values: { - xs: 0, - sm: 600, - md: 960, - lg: 1280, - xl: 1920, + shape : { borderRadius : 8, }, + breakpoints : { + values : { + xs : 0, + sm : 600, + md : 960, + lg : 1280, + xl : 1920, }, }, }); theme = { ...theme, - overrides: { - MuiSvgIcon: { - root: { + overrides : { + MuiSvgIcon : { + root : { ...iconMedium } }, - MuiDrawer: { paper: { backgroundColor: '#263238', }, }, - MuiButton: { - label: { textTransform: 'initial', }, - contained: { - boxShadow: 'none', - '&:active': { boxShadow: 'none', }, + MuiDrawer : { paper : { backgroundColor : '#263238', }, }, + MuiButton : { + label : { textTransform : 'initial', }, + contained : { + boxShadow : 'none', + '&:active' : { boxShadow : 'none', }, }, }, - MuiToggleButton: { - label: { - textTransform: 'initial', - color: '#607d8b', + MuiToggleButton : { + label : { + textTransform : 'initial', + color : '#607d8b', }, }, - MuiTabs: { - root: { marginLeft: theme.spacing(1), }, - indicator: { - height: 3, - borderTopLeftRadius: 3, - borderTopRightRadius: 3, + MuiTabs : { + root : { marginLeft : theme.spacing(1), }, + indicator : { + height : 3, + borderTopLeftRadius : 3, + borderTopRightRadius : 3, }, }, - MuiTab: { - root: { - textTransform: 'initial', - margin: '0 16px', - minWidth: 0, + MuiTab : { + root : { + textTransform : 'initial', + margin : '0 16px', + minWidth : 0, // [theme.breakpoints.up('md')]: { // minWidth: 0, // }, }, - labelContainer: { - padding: 0, + labelContainer : { + padding : 0, // [theme.breakpoints.up('md')]: { // padding: 0, // }, }, }, - MuiIconButton: { root: { padding: theme.spacing(1), }, colorPrimary: { color: "#607d8b" } }, - MuiTooltip: { tooltip: { borderRadius: 4, }, }, - MuiDivider: { root: { backgroundColor: '#404854', }, }, - MuiListItemText: { primary: { fontWeight: theme.typography.fontWeightMedium, }, }, - MuiListItemIcon: { - root: { - color: 'inherit', - marginRight: 0, - '& svg': { fontSize: 20, }, - justifyContent: 'center', - minWidth: 0 + MuiIconButton : { root : { padding : theme.spacing(1), }, colorPrimary : { color : "#607d8b" } }, + MuiTooltip : { tooltip : { borderRadius : 4, }, }, + MuiDivider : { root : { backgroundColor : '#404854', }, }, + MuiListItemText : { primary : { fontWeight : theme.typography.fontWeightMedium, }, }, + MuiListItemIcon : { + root : { + color : 'inherit', + marginRight : 0, + '& svg' : { fontSize : 20, }, + justifyContent : 'center', + minWidth : 0 }, }, - MuiAvatar: { - root: { - width: 32, - height: 32, + MuiAvatar : { + root : { + width : 32, + height : 32, }, }, // global style for body throughout meshery-ui - MuiCssBaseline: { - "@global": { - body: { - backgroundColor: "#eaeff1", + MuiCssBaseline : { + "@global" : { + body : { + backgroundColor : "#eaeff1", }, }, }, @@ -431,117 +431,117 @@ theme = { // }, // }, }, - props: { MuiTab: { disableRipple: true, }, }, - mixins: { ...theme.mixins, }, + props : { MuiTab : { disableRipple : true, }, }, + mixins : { ...theme.mixins, }, }; export default theme export const notificationColors = { - error: "#F91313", - warning: "#F0A303", - success: "#206D24", - info: "#2196F3", - darkRed: "#B32700" + error : "#F91313", + warning : "#F0A303", + success : "#206D24", + info : "#2196F3", + darkRed : "#B32700" }; export const darkNotificationColors = { - error: "#F91313", - warning: "#F0D053", - success: "#78C57C", - info: "#5FD4FF" + error : "#F91313", + warning : "#F0D053", + success : "#78C57C", + info : "#5FD4FF" }; export const styles = (theme) => ({ - root: { - display: 'flex', - minHeight: '100vh', + root : { + display : 'flex', + minHeight : '100vh', }, - drawer: { - [theme.breakpoints.up('sm')]: { - width: drawerWidth, - flexShrink: 0, + drawer : { + [theme.breakpoints.up('sm')] : { + width : drawerWidth, + flexShrink : 0, }, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.enteringScreen, + transition : theme.transitions.create('width', { + easing : theme.transitions.easing.sharp, + duration : theme.transitions.duration.enteringScreen, }), }, - drawerCollapsed: { - [theme.breakpoints.up('sm')]: { width: theme.spacing(8.4) + 1, }, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen, + drawerCollapsed : { + [theme.breakpoints.up('sm')] : { width : theme.spacing(8.4) + 1, }, + transition : theme.transitions.create('width', { + easing : theme.transitions.easing.sharp, + duration : theme.transitions.duration.leavingScreen, }), - overflowX: 'hidden', + overflowX : 'hidden', }, - appContent: { - flex: 1, - display: 'flex', - flexDirection: 'column', + appContent : { + flex : 1, + display : 'flex', + flexDirection : 'column', }, - mainContent: { - flex: 1, - padding: '48px 36px 24px', + mainContent : { + flex : 1, + padding : '48px 36px 24px', }, - footer: { - backgroundColor: '#fff', - padding: theme.spacing(2), + footer : { + backgroundColor : '#fff', + padding : theme.spacing(2), }, - footerDark: { - backgroundColor: '#202020', - padding: theme.spacing(2), + footerDark : { + backgroundColor : '#202020', + padding : theme.spacing(2), }, - footerText: { - cursor: 'pointer', - display: 'inline', - verticalAlign: 'middle', + footerText : { + cursor : 'pointer', + display : 'inline', + verticalAlign : 'middle', }, - footerIcon: { - display: 'inline', - verticalAlign: 'bottom' + footerIcon : { + display : 'inline', + verticalAlign : 'bottom' }, - icon: { fontSize: 20, }, - notifSuccess: { - backgroundColor: "rgb(248, 252, 248) !important", - color: `${notificationColors.success} !important`, pointerEvents: "auto !important" + icon : { fontSize : 20, }, + notifSuccess : { + backgroundColor : "rgb(248, 252, 248) !important", + color : `${notificationColors.success} !important`, pointerEvents : "auto !important" }, - notifInfo: { - backgroundColor: "rgb(248, 252, 248) !important", - color: `${notificationColors.info} !important`, pointerEvents: "auto !important" + notifInfo : { + backgroundColor : "rgb(248, 252, 248) !important", + color : `${notificationColors.info} !important`, pointerEvents : "auto !important" }, - notifWarn: { - backgroundColor: "#fff !important", - color: `${notificationColors.warning} !important`, pointerEvents: "auto !important" + notifWarn : { + backgroundColor : "#fff !important", + color : `${notificationColors.warning} !important`, pointerEvents : "auto !important" }, - notifError: { - backgroundColor: "rgb(248, 252, 248) !important", - color: `${notificationColors.error} !important`, - pointerEvents: "auto !important" + notifError : { + backgroundColor : "rgb(248, 252, 248) !important", + color : `${notificationColors.error} !important`, + pointerEvents : "auto !important" }, - darknotifSuccess: { - backgroundColor: "#323232 !important", - color: `${darkNotificationColors.success} !important`, - pointerEvents: "auto !important" + darknotifSuccess : { + backgroundColor : "#323232 !important", + color : `${darkNotificationColors.success} !important`, + pointerEvents : "auto !important" }, - darknotifInfo: { - backgroundColor: "#323232 !important", - color: `${darkNotificationColors.info} !important`, - pointerEvents: "auto !important" + darknotifInfo : { + backgroundColor : "#323232 !important", + color : `${darkNotificationColors.info} !important`, + pointerEvents : "auto !important" }, - darknotifWarn: { - backgroundColor: "#323232 !important", - color: `${darkNotificationColors.warning} !important`, - pointerEvents: "auto !important" + darknotifWarn : { + backgroundColor : "#323232 !important", + color : `${darkNotificationColors.warning} !important`, + pointerEvents : "auto !important" }, - darknotifError: { - backgroundColor: "#323232 !important", - color: `${darkNotificationColors.error} !important`, - pointerEvents: "auto !important" + darknotifError : { + backgroundColor : "#323232 !important", + color : `${darkNotificationColors.error} !important`, + pointerEvents : "auto !important" }, - playgroundFooter: { - backgroundColor: notificationColors.warning, - padding: theme.spacing(2), + playgroundFooter : { + backgroundColor : notificationColors.warning, + padding : theme.spacing(2), } }); \ No newline at end of file diff --git a/ui/utils/hooks/useNotification.js b/ui/utils/hooks/useNotification.js index cea528fb4b2..6f96fd04f2e 100644 --- a/ui/utils/hooks/useNotification.js +++ b/ui/utils/hooks/useNotification.js @@ -64,8 +64,8 @@ export const useNotification = () => { message, { //NOTE: Need to Consolidate the variant and event_type - variant: typeof event_type === "string" ? event_type : event_type?.type, - action: function Action(key) { + variant : typeof event_type === "string" ? event_type : event_type?.type, + action : function Action(key) { return ( {showInNotificationCenter && From 596bc15cd91af6c9b6aeb92637e0f23206b85ded Mon Sep 17 00:00:00 2001 From: theBeginner86 Date: Mon, 18 Sep 2023 21:22:19 +0000 Subject: [PATCH 39/44] [Docs] Release Notes for Meshery Signed-off-by: l5io --- docs/_releases/v0.6.141.md | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/_releases/v0.6.141.md diff --git a/docs/_releases/v0.6.141.md b/docs/_releases/v0.6.141.md new file mode 100644 index 00000000000..55305626164 --- /dev/null +++ b/docs/_releases/v0.6.141.md @@ -0,0 +1,40 @@ +--- +layout: release +date: 2023-09-18 +tag: v0.6.141 +--- + +## What's New +## 🔤 General +## ⌨️ Meshery CLI + +- [bug] Update healthcheck.go @theBeginner86 (#8770) + +## 🖥 Meshery UI + +- [chore] Fix lint @theBeginner86 (#8821) +- [UI] Added Connect Helm Repo button in connections page @Yashsharma1911 (#8816) +- Format k8s response @MUzairS15 (#8775) +- Notification Center @aabidsofi19 (#8733) +- Bump @cypress/request and cypress in /ui @dependabot (#8792) +- [UI] Prevent rjsf modal rerendering @Yashsharma1911 (#8787) + +## 🐛 Bug Fixes + +- [bug] Update healthcheck.go @theBeginner86 (#8770) + +## 🧰 Maintenance + +- [chore] Fix lint @theBeginner86 (#8821) +- Bump @cypress/request and cypress in /ui @dependabot (#8792) + +## 📖 Documentation + +- Notification Center @aabidsofi19 (#8733) +- Fixes the issue in Docs Search: Unwanted display of search result on separate line from bullet point in safari browser(7402) @phanithinks (#8690) +- meshmodel docs @parth721 (#8656) + +## 👨🏽‍💻 Contributors + +Thank you to our contributors for making this release possible: +@MUzairS15, @Yashsharma1911, @aabidsofi19, @alphaX86, @dependabot, @leecalcote, @parth721, @phanithinks and @theBeginner86 From adc7c2783d9cbfb3974419cd5ab324ad86107b12 Mon Sep 17 00:00:00 2001 From: Mohith234 Date: Tue, 19 Sep 2023 14:19:57 +0530 Subject: [PATCH 40/44] Update meshery deployment incompatible error docs Signed-off-by: Mohith234 --- CONTRIBUTING.md | 7 +++---- docs/pages/project/contributing/contributing-ui.md | 9 +++++---- docs/pages/project/contributing/meshery-server.md | 10 +++++++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0861d47b660..da6e6616877 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,10 +137,9 @@ Potential Solution: - Go to your meshery folder in your local-system where you’ve cloned it. Execute: - -- Restart the meshery server. -- Make sure that `BUILD="v0.X.X"` is logged in some starting line while starting the server. - +- `git remote add upstream https://github.com/meshery/meshery` +- `git fetch upstream` +- Restart the meshery server - Addionally, before restarting the server, if you like to pull the latest changes, you can do: `git pull upstream master` ### UI Development Server diff --git a/docs/pages/project/contributing/contributing-ui.md b/docs/pages/project/contributing/contributing-ui.md index 9b8e66f63f0..41b7c8d0db7 100644 --- a/docs/pages/project/contributing/contributing-ui.md +++ b/docs/pages/project/contributing/contributing-ui.md @@ -98,12 +98,13 @@ Now, Meshery will run on the default port `http://localhost:9081`. Potential Solution: -- Go to your `meshery` clone. +- Go to your meshery folder in your local-system where you’ve cloned it. Execute: - -- Restart Meshery Server. -- `export BUILD="v0.X.X"`before building Meshery Server again. +- `git remote add upstream https://github.com/meshery/meshery` +- `git fetch upstream` +- Restart the meshery server +- Addionally, before restarting the server, if you like to pull the latest changes, you can do: `git pull upstream master` ### UI Development Server diff --git a/docs/pages/project/contributing/meshery-server.md b/docs/pages/project/contributing/meshery-server.md index bdb87d88005..aeb5bd27ff8 100644 --- a/docs/pages/project/contributing/meshery-server.md +++ b/docs/pages/project/contributing/meshery-server.md @@ -40,10 +40,14 @@ After running Meshery server, you will need to select your **Cloud Provider** by Potential Solution: -From the root of your cloned repo, execute: -- `CTL+C` to stop Meshery Server. -- `export BUILD="v0.X.X"` prior to building Meshery Server again, setting this value to the desired Meshery version (git tag). +- Go to your meshery folder in your local-system where you’ve cloned it. +Execute: + +- `git remote add upstream https://github.com/meshery/meshery` +- `git fetch upstream` +- Restart the meshery server +- Addionally, before restarting the server, if you like to pull the latest changes, you can do: `git pull upstream master` #### Building Docker image To build a Docker image of Meshery, please ensure you have `Docker` installed to be able to build the image. Now, run the following command to build the Docker image: From 817f2c6626e329d02f23a8b9ab47ab8d828de956 Mon Sep 17 00:00:00 2001 From: sudhanshu dasgupta Date: Tue, 19 Sep 2023 16:18:56 +0530 Subject: [PATCH 41/44] fix refetching bug Signed-off-by: sudhanshu dasgupta --- ui/components/connections/index.js | 22 +++++++++---------- ui/utils/custom-search.js | 34 +++++++++++++++--------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/ui/components/connections/index.js b/ui/components/connections/index.js index 74dde0efc3a..c7920a06d1b 100644 --- a/ui/components/connections/index.js +++ b/ui/components/connections/index.js @@ -429,22 +429,22 @@ function Connections({ page, print : false, download : false, + selectableRows : "none", textLabels : { selectedRows : { text : "connection(s) selected", }, }, + selectToolbarPlacement : "none", + enableNestedDataAccess : ".", - onSearchClose : () => { - setSearch(""); - }, onTableChange : (action, tableState) => { switch (action) { case "changePage": - getConnections(tableState.page.toString(), pageSize.toString()); + getConnections(tableState.page.toString(), pageSize.toString(), search); break; case "changeRowsPerPage": - getConnections(page.toString(), tableState.rowsPerPage.toString()); + getConnections(page.toString(), tableState.rowsPerPage.toString(), search); break; case "search": if (searchTimeout.current) { @@ -465,18 +465,18 @@ function Connections({ isRowExpandable : () => { return true; }, - rowsExpanded : [0, 1], + // rowsExpanded : [0, 1], renderExpandableRow : (_, tableMeta) => { return ( - + Server Build SHA: {connections ? connections[tableMeta.rowIndex]?.metadata?.server_build_sha : "-"} - + Server Version: {connections ? connections[tableMeta.rowIndex]?.metadata?.server_version : "-"} - + {/* */} ); }, @@ -498,7 +498,7 @@ function Connections({ method : "GET", }, (res) => { - setConnections(res?.connections); + setConnections(res?.connections || []); setPage(res?.page || 0); setCount(res?.total_count || 0); setPageSize(res?.page_size || 0); @@ -579,7 +579,7 @@ function Connections({ setSearch(value); getConnections(page, pageSize, value); }} - placeholder="Search" + placeholder="Search connections..." /> { }} /> - - {expanded ? ( + {expanded ? ( + { - ) : ( - - + ) : ( + + - - - - )} - + } + > + + + + )}
    ); }; From 24f15d2860e9b96e356e419dc163c201b95e3739 Mon Sep 17 00:00:00 2001 From: Lee Calcote Date: Tue, 19 Sep 2023 10:36:28 -0500 Subject: [PATCH 42/44] Update color of "No notifications to show" Signed-off-by: Lee Calcote --- ui/components/NotificationCenter/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index f4399af8d1c..061ad1511e7 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -26,7 +26,7 @@ const EmptyState = () => { return ( - No notifications to show + No notifications to show ) } From c046b85fda14117acb01878497d53b0c9c91351c Mon Sep 17 00:00:00 2001 From: Pranav Singh Date: Wed, 20 Sep 2023 00:39:57 +0530 Subject: [PATCH 43/44] fix color undefined Signed-off-by: Pranav Singh --- ui/components/NotificationCenter/index.js | 2 +- ui/components/NotificationCenter/notification.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js index f4399af8d1c..4eda28334b6 100644 --- a/ui/components/NotificationCenter/index.js +++ b/ui/components/NotificationCenter/index.js @@ -58,7 +58,7 @@ const NavbarNotificationIcon = () => { const NotificationCountChip = ({ classes, notificationStyle, count,type, handleClick }) => { const chipStyles = { - fill : notificationStyle.color, + fill : notificationStyle?.color, height : "20px", width : "20px", } diff --git a/ui/components/NotificationCenter/notification.js b/ui/components/NotificationCenter/notification.js index f32e528cd4b..4cc8bb44a9e 100644 --- a/ui/components/NotificationCenter/notification.js +++ b/ui/components/NotificationCenter/notification.js @@ -254,8 +254,8 @@ export const Notification = ({ event_id }) => { const isVisible = event.is_visible === undefined ? true : event.is_visible const severityStyles = SEVERITY_STYLE[event.severity] const classes = useStyles({ - notificationColor : severityStyles.color, - status : event.status + notificationColor : severityStyles?.color, + status : event?.status }) const [expanded, setExpanded] = React.useState(false) const handleExpandClick = (e) => { @@ -278,7 +278,7 @@ export const Notification = ({ event_id }) => {
    - + {event.description} From 8aaa09836a4f029eee1e94e9ec3f6303d404d9d7 Mon Sep 17 00:00:00 2001 From: theBeginner86 Date: Tue, 19 Sep 2023 19:14:13 +0000 Subject: [PATCH 44/44] [Docs] Release Notes for Meshery Signed-off-by: l5io --- docs/_releases/v0.6.142.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/_releases/v0.6.142.md diff --git a/docs/_releases/v0.6.142.md b/docs/_releases/v0.6.142.md new file mode 100644 index 00000000000..ef0fdc59c09 --- /dev/null +++ b/docs/_releases/v0.6.142.md @@ -0,0 +1,35 @@ +--- +layout: release +date: 2023-09-19 +tag: v0.6.142 +--- + +## What's New +## 🔤 General +- Remove adapter references in default env vars @leecalcote (#8810) +- [Docker Extension] Remove adapters from default configuration @leecalcote (#8811) + +## 🖥 Meshery UI + +- [chore] fix color undefined @theBeginner86 (#8832) +- [UI] Notification Center: Update color of "No notifications to show" @leecalcote (#8828) +- Fix repetitive api calls and UI issues in connections page @sudhanshutech (#8825) +- changed LoadTestTimerConfig and MeshAdapterConfigComponent to functional component @EraKin575 (#8781) + +## 🐛 Bug Fixes + +- [chore] fix color undefined @theBeginner86 (#8832) +- Fix repetitive api calls and UI issues in connections page @sudhanshutech (#8825) + +## 🧰 Maintenance + +- Meshery End-to-End Tests with mesheryctl @KiptoonKipkurui (#8808) + +## 📖 Documentation + +- Update "Meshery Deployment Incompatible" error docs @Mohith234 (#8824) + +## 👨🏽‍💻 Contributors + +Thank you to our contributors for making this release possible: +@EraKin575, @KiptoonKipkurui, @Mohith234, @Yashsharma1911, @aabidsofi19, @leecalcote, @sudhanshutech and @theBeginner86