diff --git a/.github/workflows/integrations-updater.yml b/.github/workflows/integrations-updater.yml index 8661dea9ad0..4d75f4c85f8 100644 --- a/.github/workflows/integrations-updater.yml +++ b/.github/workflows/integrations-updater.yml @@ -38,7 +38,7 @@ jobs: token: ${{ secrets.GH_ACCESS_TOKEN }} path: meshery.io - name: Run utility - run: cd meshery/scripts/component_updater; go build main.go; ./main "https://docs.google.com/spreadsheets/d/e/2PACX-1vSgOXuiqbhUgtC9oNbJlz9PYpOEaFVoGNUFMIk4NZciFfQv1ewZg8ahdrWHKI79GkKK9TbmnZx8CqIe/pub?gid=0&single=true&output=csv" --system docs layer5/src/collections/integrations meshery.io/integrations --published-only + run: cd meshery/scripts/component_updater; go build main.go; ./main "https://docs.google.com/spreadsheets/d/e/2PACX-1vSgOXuiqbhUgtC9oNbJlz9PYpOEaFVoGNUFMIk4NZciFfQv1ewZg8ahdrWHKI79GkKK9TbmnZx8CqIe/pub?gid=0&single=true&output=csv" --system docs layer5/src/collections/integrations meshery.io/integrations docs/ --published-only - name: Commit changes to Layer5.io repo uses: stefanzweifel/git-auto-commit-action@v4 with: diff --git a/docs/_releases/v0.6.140.md b/docs/_releases/v0.6.140.md new file mode 100644 index 00000000000..0bb4c8afb7b --- /dev/null +++ b/docs/_releases/v0.6.140.md @@ -0,0 +1,31 @@ +--- +layout: release +date: 2023-09-16 +tag: v0.6.140 +--- + +## What's New +## 🔤 General +## 🖥 Meshery UI + +- [UI] Convert Index.js and settings.js from class-based component to functional component @ShatilKhan (#8705) +- Fix crash on saving applications @sudhanshutech (#8776) +- updated filter.js to functional component @abhijeetgauravm (#8785) +- Updated PromptComponent to support checkbox @Yashsharma1911 (#8672) +- chore: sort the technology list in alphabetical order @Yana-Gupta (#8708) +- Refactor /management/index.js to functional component @dhiraj0911 (#8703) +- [UI] Transparent "WA" Filter Icon @Chadha93 (#8793) +- Fix infinite publish requests @aabidsofi19 (#8769) + +## 🐛 Bug Fixes + +- Fix crash on saving applications @sudhanshutech (#8776) + +## 🧰 Maintenance + +- Fix failing updater test @KiptoonKipkurui (#8791) + +## 👨🏽‍💻 Contributors + +Thank you to our contributors for making this release possible: +@Chadha93, @KiptoonKipkurui, @ShatilKhan, @Yana-Gupta, @Yashsharma1911, @aabidsofi19, @abhijeetgauravm, @dhiraj0911, @leecalcote, @sudhanshutech and l5io diff --git a/docs/_sass/overview.scss b/docs/_sass/overview.scss index 0053d111bf8..1861eb8cf54 100644 --- a/docs/_sass/overview.scss +++ b/docs/_sass/overview.scss @@ -31,7 +31,7 @@ div.overview { h2[id]:before, h4[id]:before, h5[id]:before { - display: block; + display: inline-block; content: " "; visibility: hidden; } @@ -83,4 +83,4 @@ a:hover, a > strong:hover { .image-right { float: right; padding-right: 10px; -} +} \ No newline at end of file diff --git a/docs/assets/img/syscmd/channel set.png b/docs/assets/img/syscmd/channel set.png new file mode 100644 index 00000000000..a79bc88b625 Binary files /dev/null and b/docs/assets/img/syscmd/channel set.png differ diff --git a/docs/assets/img/syscmd/channel view.png b/docs/assets/img/syscmd/channel view.png new file mode 100644 index 00000000000..2ddbaca9e26 Binary files /dev/null and b/docs/assets/img/syscmd/channel view.png differ diff --git a/docs/assets/img/syscmd/context create.png b/docs/assets/img/syscmd/context create.png new file mode 100644 index 00000000000..7a229978bca Binary files /dev/null and b/docs/assets/img/syscmd/context create.png differ diff --git a/docs/assets/img/syscmd/context delete.png b/docs/assets/img/syscmd/context delete.png new file mode 100644 index 00000000000..c430fe14414 Binary files /dev/null and b/docs/assets/img/syscmd/context delete.png differ diff --git a/docs/assets/img/syscmd/context flag.png b/docs/assets/img/syscmd/context flag.png new file mode 100644 index 00000000000..0f1442833c4 Binary files /dev/null and b/docs/assets/img/syscmd/context flag.png differ diff --git a/docs/assets/img/syscmd/context list.png b/docs/assets/img/syscmd/context list.png new file mode 100644 index 00000000000..024de88623a Binary files /dev/null and b/docs/assets/img/syscmd/context list.png differ diff --git a/docs/assets/img/syscmd/context view.png b/docs/assets/img/syscmd/context view.png new file mode 100644 index 00000000000..eb86563b21f Binary files /dev/null and b/docs/assets/img/syscmd/context view.png differ diff --git a/docs/assets/img/syscmd/dashboard skip.png b/docs/assets/img/syscmd/dashboard skip.png new file mode 100644 index 00000000000..aa7d8552ad3 Binary files /dev/null and b/docs/assets/img/syscmd/dashboard skip.png differ diff --git a/docs/assets/img/syscmd/force stop.png b/docs/assets/img/syscmd/force stop.png new file mode 100644 index 00000000000..1dd67dd46b3 Binary files /dev/null and b/docs/assets/img/syscmd/force stop.png differ diff --git a/docs/assets/img/syscmd/keep namespace.png b/docs/assets/img/syscmd/keep namespace.png new file mode 100644 index 00000000000..56d4975765d Binary files /dev/null and b/docs/assets/img/syscmd/keep namespace.png differ diff --git a/docs/assets/img/syscmd/platform.png b/docs/assets/img/syscmd/platform.png new file mode 100644 index 00000000000..81d94f4a2bc Binary files /dev/null and b/docs/assets/img/syscmd/platform.png differ diff --git a/docs/assets/img/syscmd/portforward.png b/docs/assets/img/syscmd/portforward.png new file mode 100644 index 00000000000..8b9922de199 Binary files /dev/null and b/docs/assets/img/syscmd/portforward.png differ diff --git a/docs/assets/img/syscmd/pro list.png b/docs/assets/img/syscmd/pro list.png new file mode 100644 index 00000000000..5d12fe1ecac Binary files /dev/null and b/docs/assets/img/syscmd/pro list.png differ diff --git a/docs/assets/img/syscmd/pro set.png b/docs/assets/img/syscmd/pro set.png new file mode 100644 index 00000000000..4e648de34f9 Binary files /dev/null and b/docs/assets/img/syscmd/pro set.png differ diff --git a/docs/assets/img/syscmd/pro switch.png b/docs/assets/img/syscmd/pro switch.png new file mode 100644 index 00000000000..69d2c9a4dd6 Binary files /dev/null and b/docs/assets/img/syscmd/pro switch.png differ diff --git a/docs/assets/img/syscmd/pro view.png b/docs/assets/img/syscmd/pro view.png new file mode 100644 index 00000000000..f35a7ea0ee9 Binary files /dev/null and b/docs/assets/img/syscmd/pro view.png differ diff --git a/docs/assets/img/syscmd/reset.png b/docs/assets/img/syscmd/reset.png new file mode 100644 index 00000000000..acd17baab4c Binary files /dev/null and b/docs/assets/img/syscmd/reset.png differ diff --git a/docs/assets/img/syscmd/restart.png b/docs/assets/img/syscmd/restart.png new file mode 100644 index 00000000000..e742d0015de Binary files /dev/null and b/docs/assets/img/syscmd/restart.png differ diff --git a/docs/assets/img/syscmd/skipbrowser.png b/docs/assets/img/syscmd/skipbrowser.png new file mode 100644 index 00000000000..225610ae6da Binary files /dev/null and b/docs/assets/img/syscmd/skipbrowser.png differ diff --git a/docs/assets/img/syscmd/start.png b/docs/assets/img/syscmd/start.png new file mode 100644 index 00000000000..da732b33847 Binary files /dev/null and b/docs/assets/img/syscmd/start.png differ diff --git a/docs/assets/img/syscmd/stop reset.png b/docs/assets/img/syscmd/stop reset.png new file mode 100644 index 00000000000..c804e97f2b6 Binary files /dev/null and b/docs/assets/img/syscmd/stop reset.png differ diff --git a/docs/assets/img/syscmd/stop.png b/docs/assets/img/syscmd/stop.png new file mode 100644 index 00000000000..68650368f04 Binary files /dev/null and b/docs/assets/img/syscmd/stop.png differ diff --git a/docs/assets/img/syscmd/system check.png b/docs/assets/img/syscmd/system check.png new file mode 100644 index 00000000000..45b94b7fad5 Binary files /dev/null and b/docs/assets/img/syscmd/system check.png differ diff --git a/docs/assets/img/syscmd/system dahboard.png b/docs/assets/img/syscmd/system dahboard.png new file mode 100644 index 00000000000..d863b09e0d4 Binary files /dev/null and b/docs/assets/img/syscmd/system dahboard.png differ diff --git a/docs/assets/img/syscmd/system login.png b/docs/assets/img/syscmd/system login.png new file mode 100644 index 00000000000..017833305bb Binary files /dev/null and b/docs/assets/img/syscmd/system login.png differ diff --git a/docs/assets/img/syscmd/system status.png b/docs/assets/img/syscmd/system status.png new file mode 100644 index 00000000000..8ae2bfe989d Binary files /dev/null and b/docs/assets/img/syscmd/system status.png differ diff --git a/docs/assets/img/syscmd/system update.png b/docs/assets/img/syscmd/system update.png new file mode 100644 index 00000000000..fdbe7b2ab6e Binary files /dev/null and b/docs/assets/img/syscmd/system update.png differ diff --git a/docs/assets/img/syscmd/update skip reset.png b/docs/assets/img/syscmd/update skip reset.png new file mode 100644 index 00000000000..c271a494bcf Binary files /dev/null and b/docs/assets/img/syscmd/update skip reset.png differ diff --git a/docs/pages/guides/mesheryctl/system-commands.md b/docs/pages/guides/mesheryctl/system-commands.md new file mode 100644 index 00000000000..60e27c29dea --- /dev/null +++ b/docs/pages/guides/mesheryctl/system-commands.md @@ -0,0 +1,183 @@ +--- +layout : default +title : Mesheryctl system commands +permalink : guides/mesheryctl/system-commands +language : en +type : Guides +category : mesheryctl +--- + +Let's get familiar with mesheryctl system commands. The syntax of the mesheryctl commands goes as follws : `mesheryctl ` + +## Main_command : system +### start +`mesheryctl system start` : This will initiate Meshery & automatically open it in your default web browser. + +skip-browser + +`mesheryctl system start --skip-browser` : It skips opening Meshery in your browser with the provided URL. + +skip-browser + +`mesheryctl system start --skip-update` : It is used when you want to skip updating Meshery if an update is available. + +skip-browser + +`mesheryctl system start --reset` : It resets your Meshery configuration file to its default configuration. + +`mesheryctl system start --platform string` : It allows you specify a platform for deploying Meshery. + +skip-browser + + +### stop +`mesheryctl system stop` : It stops Meshery resources & delete its associated namespaces. + +skip-browser + +`mesheryctl system stop --reset` : It stops Meshery and resets the Meshery configuration file to its default configuration. + +skip-browser + +`mesheryctl system stop --keep-namespace` : It stops Meshery without deleting the associated namespaces. + +skip-browser + +`mesheryctl system stop --force` : Force stops Meshery instead of gentle way. This is only used in emergency situations when `mesheryctl system stop` can't halt Meshery. + +skip-browser + +### update +`mesheryctl system update` : This updates Meshery itself, not the mesheryctl. Ensure Meshery is running when using this. + +skip-browser + +`mesheryctl system update --skip-reset` : Skips the check for a new manifest file. + +skip-browser + +### reset +`mesheryctl system reset` : Resets Meshery to its default configuration. + +skip-browser + +### restart +`meshryctl system restart` : Stops Meshery and then starts it again. Opens the website in your default browser. + +skip-browser + +### status +`mesheryctl system status` : Displays the status of Meshery components. + +`mesheryctl system status --verbose` : Provides additional data along with Meshery and its component status. + +skip-browser + + +### dashboard +`mesheryctl system dashboard` : Opens the Meshery dashboard in your default browser. + +skip-browser + +`mesheryctl system dashboard --skip-browser` : Provides the link to the dashboard, allowing you to open it in any browser. + +skip-browser + +`mesheryctl system dashboard --port-forward` : If the current port is busy, it opens the dashboard on another port. + +skip-browser + + +### login +`mesheryctl system login` : Authenticates you with your selected provider. + +skip-browser + +### check +`mesheryctl system check` : Performs checks for both pre & post mesh deployment scenarios on Meshery. + +skip-browser + +`mesheryctl system check --preflight` : Runs pre-deployment checks. + +`mesheryctl system check --adapter` : Runs checks for a specific Mesh adapter. + +`mesheryctl system check --adapters` : Runs checks for Meshery adapters + +`mesheryctl system check --components` : Runs checks for Meshery components + +`mesheryctl system check --operator` : Runs checks for Meshery Operator + +## Main_command : system channel +### channel +`mesheyctl system channel set [stable|stable-version|edge|edge-version]` : Used to set the channel. + +`mesheyctl system channel switch [stable|stable-version|edge|edge-version]` : Used to switch between channels. + +skip-browser + +`mesheyctl system channel view --all` : Displays all available channels. + +`mesheyctl system channel view` : Displays the current channel. + +skip-browser + + +## Main_command : system context +### create +`mesheryctl system context create 'context-name'` : Creates a new context with default parameters. + +skip-browser + +`mesheryctl system context create --component stringArray` : Specifies the component to be created in the context. + +`mesheryctl system context create --platform string` : Specifies the platform. + +`mesheryctl system context create --set` : Sets this context as default context. + +`mesheryctl system context create --url string` : Specifies the target URL. + +skip-browser + + +### switch +`mesheryctl system context switch` : Easily switch between different contexts. + +### list +`mesheryctl system context list` : Lists all your available Meshery contexts. + +skip-browser + +### delete +`mesheryctl system context delete` : Delete context. + +skip-browser + + +### view +`mesheryctl system context view` : Display all your contexts with additional information. + +skip-browser + + +## Main_command : system provider +### switch +`mesheryctl system provider switch` : Changes your provider + +skip-browser + +### list +`mesheryctl system provider list` : Lists all available providers + +skip-browser + +### set +`mesheryctl system provider set` : Set your provider + +skip-browser + +### view +`mesheryctl system provider view` : Lists your current context and provider + +skip-browser + diff --git a/docs/pages/project/contributing/contributing-ui-notification-center.md b/docs/pages/project/contributing/contributing-ui-notification-center.md new file mode 100644 index 00000000000..6e0f792ae1f --- /dev/null +++ b/docs/pages/project/contributing/contributing-ui-notification-center.md @@ -0,0 +1,171 @@ +--- +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 +--- + +

Prerequisite Reading

+
  1. Contributing to Meshery UI
+
+ +## Contributing to Meshery UI - 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 Severities and Colors + +Notification severities and colors are defined in the constants file, `ui/components/NotificationCenter/constants.js`. + +### Notification Filtering and Searching + +**Table of Contents** + +- [Usage](#usage) +- [Props](#props) +- [Examples](#examples) + +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: + +```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 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** + +- [Overview](#overview) +- [State Definitions](#state-definitions) +- [Reducers](#reducers) +- [State Transitions](#state-transitions) +- [Initial State Handling](#initial-state-handling) + +### 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 suggested-reading.html %} diff --git a/server/handlers/error.go b/server/handlers/error.go index a40d14d815c..0a7564d59e7 100644 --- a/server/handlers/error.go +++ b/server/handlers/error.go @@ -420,7 +420,7 @@ func ErrDeleteFilter(err error) error { } func ErrSavePattern(err error) error { - return errors.New(ErrSavePatternCode, errors.Alert, []string{"Error failed to save pattern"}, []string{err.Error()}, []string{"Cannot save the Pattern due to wrong path or URL"}, []string{"Check if the given path or URL of the Pattern is correct"}) + return errors.New(ErrSavePatternCode, errors.Alert, []string{"Error failed to save design"}, []string{err.Error()}, []string{"Cannot save the design due to an invalid path or URL"}, []string{"Confirm the correct path / URL to the design"}) } func ErrSaveApplication(err error) error { @@ -432,7 +432,7 @@ func ErrFetchApplication(err error) error { } func ErrDeleteApplication(err error) error { - return errors.New(ErrDeleteApplicationCode, errors.Alert, []string{"Error failed to delete application"}, []string{err.Error()}, []string{"Application might already have been deleted", "You might not have enough permissions to perform the operation.", }, []string{"Check the owner of the application."}) + return errors.New(ErrDeleteApplicationCode, errors.Alert, []string{"Error failed to delete application"}, []string{err.Error()}, []string{"Application might already have been deleted", "You might not have enough permissions to perform the operation."}, []string{"Check the owner of the application."}) } func ErrGetPattern(err error) error { @@ -520,11 +520,11 @@ func ErrGetEvents(err error) error { } func ErrUpdateEvent(err error, id string) error { - return errors.New(ErrUpdateEventCode, errors.Alert, []string{fmt.Sprintf("Could not update event status for %s", id)}, []string{err.Error()}, []string{"Provided event status not supported", "Event has been deleted or does not exist", "Database is corrupt."}, []string{"Verify event filter settings","Reset database."}) + return errors.New(ErrUpdateEventCode, errors.Alert, []string{fmt.Sprintf("Could not update event status for %s", id)}, []string{err.Error()}, []string{"Provided event status not supported", "Event has been deleted or does not exist", "Database is corrupt."}, []string{"Verify event filter settings", "Reset database."}) } func ErrDeleteEvent(err error, id string) error { - return errors.New(ErrDeleteEventCode, errors.Alert, []string{fmt.Sprintf("Could not delete event %s", id)}, []string{err.Error()}, []string{"Event might have been deleted and doesn't exist", "Database is corrupt."}, []string{"Verify event filter settings","Reset database."}) + return errors.New(ErrDeleteEventCode, errors.Alert, []string{fmt.Sprintf("Could not delete event %s", id)}, []string{err.Error()}, []string{"Event might have been deleted and doesn't exist", "Database is corrupt."}, []string{"Verify event filter settings", "Reset database."}) } func ErrUnsupportedEventStatus(err error, status string) error { 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/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/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/ReadIcon.js b/ui/assets/icons/ReadIcon.js new file mode 100644 index 00000000000..f47b1482fd2 --- /dev/null +++ b/ui/assets/icons/ReadIcon.js @@ -0,0 +1,25 @@ +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/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/assets/icons/UnreadIcon.js b/ui/assets/icons/UnreadIcon.js new file mode 100644 index 00000000000..4c459448049 --- /dev/null +++ b/ui/assets/icons/UnreadIcon.js @@ -0,0 +1,21 @@ +import React from "react"; + +const UnreadIcon = ({ height, width, fill, style = {} }) => { + return ( + + + + + + ); +}; + +export default UnreadIcon; \ No newline at end of file diff --git a/ui/components/Filters.js b/ui/components/Filters.js index b45c727f6db..9da1d5a3340 100644 --- a/ui/components/Filters.js +++ b/ui/components/Filters.js @@ -25,7 +25,6 @@ import { toggleCatalogContent, updateProgress } from "../lib/store"; import TableSortLabel from "@material-ui/core/TableSortLabel"; import dataFetch from "../lib/data-fetch"; import PromptComponent from "./PromptComponent"; -import UploadImport from "./Modals/ImportModal"; import FullscreenIcon from '@material-ui/icons/Fullscreen'; import FullscreenExitIcon from '@material-ui/icons/FullscreenExit'; import { FILE_OPS, MesheryFiltersCatalog, VISIBILITY } from "../utils/Enum"; @@ -313,7 +312,8 @@ function MesheryFilters({ updateProgress, user, classes, selectedK8sContexts, ca async (result) => { try { const { models } = await getMeshModels(); - const modelNames = _.uniq(models?.map((model) => model.displayName)); + const modelNames = _.uniq(models?.map((model) => model.displayName.toUpperCase())); + modelNames.sort() // Modify the schema using the utility function const modifiedSchema = modifyRJSFSchema( @@ -738,16 +738,6 @@ function MesheryFilters({ updateProgress, user, classes, selectedK8sContexts, ca reader.readAsArrayBuffer(file); } - function urlUploadHandler(link, _, metadata,) { - - handleSubmit({ - data : link, - name : "meshery_" + Math.floor(trueRandom() * 100), - type : FILE_OPS.URL_UPLOAD, - metadata : metadata - }); - } - const columns = [ { name : "name", @@ -1161,9 +1151,9 @@ function MesheryFilters({ updateProgress, user, classes, selectedK8sContexts, ca canPublishFilter={canPublishFilter} handlePublish={handlePublish} handleUnpublishModal={handleUnpublishModal} + handleUploadImport={handleUploadImport} handleClone={handleClone} handleDownload={handleDownload} - urlUploadHandler={urlUploadHandler} uploadHandler={uploadHandler} setSelectedFilter={setSelectedFilter} selectedFilter={selectedFilter} @@ -1171,8 +1161,6 @@ function MesheryFilters({ updateProgress, user, classes, selectedK8sContexts, ca importSchema={importSchema} setPage={setPage} selectedPage={page} - UploadImport={UploadImport} - handleImportFilter={handleImportFilter} publishModal={publishModal} setPublishModal={setPublishModal} publishSchema={publishSchema} @@ -1190,9 +1178,9 @@ function MesheryFilters({ updateProgress, user, classes, selectedK8sContexts, ca componentCount={modalOpen.count} tab={modalOpen.deploy ? 2 : 1} /> - {canPublishFilter && + {(canPublishFilter && publishModal.open) && } - } - submitBtnIcon={} - /> - {/* REMOVE this with its deps fetchFilters(page, pageSize, search, sortOrder) } configuration="Filter" /> */} + {importModal.open && + } + submitBtnIcon={} + /> + } ); diff --git a/ui/components/Header.js b/ui/components/Header.js index 85d92b6957e..383a191d989 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'; diff --git a/ui/components/MesheryApplications.js b/ui/components/MesheryApplications.js index 6e651184b0d..f9d2aa395b8 100644 --- a/ui/components/MesheryApplications.js +++ b/ui/components/MesheryApplications.js @@ -484,7 +484,7 @@ function MesheryApplications({ { credentials : "include", method : "PUT", - body : JSON.stringify({ application_data : { id, name : metadata.name || name, application_file : data }, save : true }), + body : JSON.stringify({ application_data : { id, name : metadata?.name || name, application_file : data }, save : true }), }, () => { updateProgress({ showProgress : false }); @@ -503,11 +503,11 @@ function MesheryApplications({ let body = { save : true } if (type === FILE_OPS.FILE_UPLOAD) { body = JSON.stringify({ - ...body, application_data : { name : metadata.name || name || getRandomName(), application_file : data } + ...body, application_data : { name : metadata?.name || name || getRandomName(), application_file : data } }) } if (type === FILE_OPS.URL_UPLOAD) { - body = JSON.stringify({ ...body, url : data, name : metadata.name || name }) + body = JSON.stringify({ ...body, url : data, name : metadata?.name || name }) } dataFetch( `/api/application/${source_type}`, @@ -690,8 +690,6 @@ function MesheryApplications({ } ]; - console.log("applications", applications) - columns.forEach((column, idx) => { if (column.name === sortOrder.split(" ")[0]) { columns[idx].options.sortDirection = sortOrder.split(" ")[1]; diff --git a/ui/components/MesheryFilters/FiltersGrid.js b/ui/components/MesheryFilters/FiltersGrid.js index 5785117562b..ecb09142fcb 100644 --- a/ui/components/MesheryFilters/FiltersGrid.js +++ b/ui/components/MesheryFilters/FiltersGrid.js @@ -9,7 +9,6 @@ import { getComponentsinFile } from "../../utils/utils"; import PublishIcon from "@material-ui/icons/Publish"; import useStyles from "../MesheryPatterns/Grid.styles"; import Modal from "../Modal"; -import Filter from "../../public/static/img/drawer-icons/filter_svg.js"; import PublicIcon from '@material-ui/icons/Public'; const INITIAL_GRID_SIZE = { xl : 4, md : 6, xs : 12 }; @@ -102,21 +101,16 @@ function FiltersGrid({ pages = 1, setPage, selectedPage, - importSchema, canPublishFilter, + handleUploadImport, handlePublish, handleUnpublishModal, - handleImportFilter, publishModal, setPublishModal, publishSchema }) { const classes = useStyles(); - const [importModal, setImportModal] = useState({ - open : false, - }); - const handlePublishModal = (filter) => { if (canPublishFilter) { setPublishModal({ @@ -135,18 +129,6 @@ function FiltersGrid({ }); }; - const handleUploadImport = () => { - setImportModal({ - open : true, - }); - }; - - const handleUploadImportClose = () => { - setImportModal({ - open : false, - }); - }; - const [modalOpen, setModalOpen] = useState({ open : false, deploy : false, @@ -244,9 +226,9 @@ function FiltersGrid({ componentCount={modalOpen.count} tab={modalOpen.deploy ? 2 : 1} /> - {canPublishFilter && ( + {(canPublishFilter && publishModal.open)&& ( } /> )} - } - submitBtnIcon={} - /> ); } diff --git a/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js b/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js index e9e52485032..5302ce093f1 100644 --- a/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js +++ b/ui/components/MesheryMeshInterface/PatternService/RJSFCustomComponents/CustomBaseInput.js @@ -43,6 +43,7 @@ const BaseInput = (props) => { focused={focused} type={props.options?.inputType} key={props.id} + disabled={props?.disabled || props?.readonly} value={ props.options?.inputType === "file" ? null 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/MesheryPatterns.js b/ui/components/MesheryPatterns.js index a3095e1c530..d755c911373 100644 --- a/ui/components/MesheryPatterns.js +++ b/ui/components/MesheryPatterns.js @@ -521,7 +521,8 @@ function MesheryPatterns({ async (result) => { try { const { models } = await getMeshModels(); - const modelNames = _.uniq(models?.map((model) => model.displayName)); + const modelNames = _.uniq(models?.map((model) => model.displayName.toUpperCase())); + modelNames.sort(); // Modify the schema using the utility function const modifiedSchema = modifyRJSFSchema( @@ -1418,9 +1419,9 @@ function MesheryPatterns({ dryRunComponent={modalOpen.dryRunComponent} errors={modalOpen.errors} /> - {canPublishPattern && + {(canPublishPattern && publishModal.open) && } /> } - } - submitBtnIcon={} - /> - {/* fetchPatterns(page, pageSize, search, sortOrder)} configuration="Design" /> */} + { importModal.open && + } + submitBtnIcon={} + /> + } diff --git a/ui/components/MesheryPatterns/MesheryPatternGridView.js b/ui/components/MesheryPatterns/MesheryPatternGridView.js index f3ca7f932ca..29eec58abfb 100644 --- a/ui/components/MesheryPatterns/MesheryPatternGridView.js +++ b/ui/components/MesheryPatterns/MesheryPatternGridView.js @@ -208,9 +208,9 @@ function MesheryPatternGrid({ patterns=[], handleVerify, handlePublish, handleUn dryRunComponent={modalOpen.dryRunComponent} validationBody={modalOpen.validationBody} /> - {canPublishPattern && + {(canPublishPattern && publishModal.open) && ( @@ -207,7 +207,7 @@ function Modal(props) { }, [title]); const handleFormChange = (data) => { - setFormState(data.formData); + formStateRef.current = data; }; return ( @@ -230,7 +230,7 @@ function Modal(props) { { try { const { models } = await getMeshModels(); - const modelNames = _.uniq(models?.map((model) => model.displayName)); + const modelNames = _.uniq(models?.map((model) => model.displayName.toUpperCase())); + modelNames.sort(); // Modify the schema using the utility function const modifiedSchema = modifyRJSFSchema(result.rjsfSchema, "properties.compatibility.items.enum", modelNames); diff --git a/ui/components/NotificationCenter/Notification.js b/ui/components/NotificationCenter/Notification.js deleted file mode 100644 index 9470825def3..00000000000 --- a/ui/components/NotificationCenter/Notification.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/constants.js b/ui/components/NotificationCenter/constants.js new file mode 100644 index 00000000000..e5a1a1ae893 --- /dev/null +++ b/ui/components/NotificationCenter/constants.js @@ -0,0 +1,40 @@ +import { NOTIFICATIONCOLORS } from "../../themes" +import AlertIcon from "../../assets/icons/AlertIcon"; +import ErrorIcon from "../../assets/icons/ErrorIcon.js" +import { Colors } from "../../themes/app"; +import ReadIcon from "../../assets/icons/ReadIcon"; + +export const SEVERITY = { + INFO : "informational", + ERROR : "error", + WARNING : "warning", + // SUCCESS: "success" +} + +export const STATUS = { + READ : "read", + UNREAD : "unread", +} + +export const STATUS_STYLE = { + [STATUS.READ] : { + icon : ReadIcon, + 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 new file mode 100644 index 00000000000..6ab39deeb6e --- /dev/null +++ b/ui/components/NotificationCenter/filter.js @@ -0,0 +1,48 @@ +import { useGetEventFiltersQuery } from "../../rtk-query/notificationCenter"; +import TypingFilter from "../TypingFilter"; +import { SEVERITY, STATUS } from "./constants"; + + +const useFilterSchema = () => { + + const { data } = useGetEventFiltersQuery(); + + return { + SEVERITY : { + value : "severity", + description : "Filter by severity", + values : Object.values(SEVERITY), + }, + + STATUS : { + value : "status", + description : "Filter by status", + values : Object.values(STATUS), + multiple : false, + }, + + ACTION : { + value : "action", + values : data?.action || [], + description : "Filter by type", + }, + + AUTHOR : { + value : "author", + description : "Filter by any user or system", + }, + + CATEGORY : { + value : "category", + description : "Filter by category", + values : data?.category || [], + }, + }; +} + +const Filter = ({ handleFilter }) => { + const filterSchema = useFilterSchema(); + return ; +}; + +export default Filter; \ No newline at end of file diff --git a/ui/components/NotificationCenter/index.js b/ui/components/NotificationCenter/index.js new file mode 100644 index 00000000000..f4399af8d1c --- /dev/null +++ b/ui/components/NotificationCenter/index.js @@ -0,0 +1,346 @@ +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, Tooltip } from "@material-ui/core"; +import Filter from "./filter"; +import BellIcon from "../../assets/icons/BellIcon.js" +import { iconMedium } from "../../css/icons.styles"; +import { SEVERITY, SEVERITY_STYLE, STATUS, STATUS_STYLE } from "./constants"; +import classNames from "classnames"; +import Notification from "./notification"; +import { store } from "../../store"; +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"; +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 = () => { + + 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,type, handleClick }) => { + const chipStyles = { + fill : notificationStyle.color, + height : "20px", + width : "20px", + } + count = Number(count).toLocaleString('en', { useGrouping : true }) + return ( + + + + ) +} + +const Header = ({ handleFilter, handleClose }) => { + + const { data } = useGetEventsSummaryQuery(); + const { count_by_severity_level, total_count } = data || { + count_by_severity_level : [], + total_count : 0 + } + const classes = useStyles() + const onClickSeverity = (severity) => { + handleFilter({ + severity : [severity] + }) + } + + const onClickStatus = (status) => { + handleFilter({ + status : status + }) + } + + const archivedCount = total_count - count_by_severity_level + .reduce((acc, item) => acc + item.count, 0) + return ( +
+
+
+ +
+ Notifications +
+
+ {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} /> + +
+
+ ) +} + +const Loading = () => { + return ( + + + ) +} + + +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 (isFetching && !hasMore) { + return + } + 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 ( + <> + {events.map((event, idx) =>
+ +
)} + + {events.length === 0 && } + +
+
+ {isFetching && 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)} />)} +
+ ) + } + + 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, { isFetching }] = useLazyGetEventsQuery() + const hasMore = useSelector((state) => state.events.current_view.has_more); + + useEffect(() => { + dispatch(loadEvents(fetchEvents, 1, { + status : STATUS.UNREAD, + })) + }, []) + + const loadMore = () => { + dispatch(loadNextPage(fetchEvents)) + } + + const handleToggle = () => { + dispatch(toggleNotificationCenter()) + }; + + const handleClose = () => { + if (!isNotificationCenterOpen) { + return + } + dispatch(closeNotificationCenter()) + setAnchorEl(null); + }; + const classes = useStyles() + // const { showFullNotificationCenter } = props; + const open = Boolean(anchorEl) || isNotificationCenterOpen; + + + const handleFilter = (filters) => { + dispatch(loadEvents(fetchEvents, 1, filters)) + } + + 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"), +// }; +// }; +const NotificationCenter = (props) => { + + return ( + <> + + + + + ) + +}; + +export default NotificationCenter; \ 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..f32e528cd4b --- /dev/null +++ b/ui/components/NotificationCenter/notification.js @@ -0,0 +1,360 @@ +import * as React from 'react'; +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 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'; +import { useUpdateStatusMutation, useDeleteEventMutation } from "../../rtk-query/notificationCenter" +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 ReadIcon from '../../assets/icons/ReadIcon'; +import UnreadIcon from '../../assets/icons/UnreadIcon'; + +const useStyles = makeStyles(() => ({ + root : (props) => ({ + width : "100%", + borderRadius : "0.25rem", + border : `0.1rem solid ${props.notificationColor}`, + borderLeftWidth : props.status === STATUS.READ ? "0.25rem" : "0.1rem", + marginBlock : "0.5rem", + }), + + summary : (props) => ({ + paddingBlock : "0.5rem", + cursor : "pointer", + backgroundColor : alpha(props.notificationColor, 0.20), + }), + + gridItem : { + display : "flex", + alignItems : "center", + justifyContent : "center", + }, + + message : { + overflow : "hidden", + textOverflow : "ellipsis", + whiteSpace : "nowrap", + overflowWrap : "break-word", + // max of min of 20rem or 50vw + maxWidth : "min(25rem, 50vw)", + width : "100%", + }, + expanded : { + paddingBlock : "0.75rem", + paddingInline : "0.2rem", + }, + actorAvatar : { + display : "flex", + justifyContent : "center", + alignItems : "start", + }, + + descriptionHeading : { + fontWeight : "bolder !important", + textTransform : "uppercase", + }, + + +})) + +const useMenuStyles = makeStyles((theme) => { + return { + paper : { + color : theme.palette.secondary.iconMain, + boxShadow : theme.shadows[4], + borderRadius : "0.25", + paddingInline : "0.5rem", + paddingBlock : "0.25rem", + width : "12.5rem", + }, + + list : { + padding : "0.5rem", + display : "flex", + flexDirection : "column", + gridGap : "0.5rem", + marginBlock : "0.5rem", + borderRadius : "0.25rem", + backgroundColor : theme.palette.secondary.honeyComb, + }, + + listItem : { + display : "flex", + gridGap : "0.5rem", + alignItems : "center", + // justifyContent: "center", + }, + + button : { + padding : "0.2rem", + display : "flex", + alignItems : "center", + justifyContent : "start", + }, + + } +}) + + +const formatTimestamp = (utcTimestamp) => { + const currentUtcTimestamp = moment.utc().valueOf() + + + const timediff = currentUtcTimestamp - moment(utcTimestamp).valueOf() + if (timediff >= 24 * 60 * 60 * 1000) { + return moment(utcTimestamp).local().format('MMM DD, YYYY') + } + return moment(utcTimestamp).fromNow() +} + +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 = (e) => { + e.stopPropagation() + setAnchorEl(null); + }; + + const [isSocialShareOpen, setIsSocialShareOpen] = React.useState(false) + const toggleSocialShare = (e) => { + e.stopPropagation() + setIsSocialShareOpen(prev => !prev) + } + + const theme = useTheme() + return ( +
e.stopPropagation()} > + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + +
+
+
+ ); +} + +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] = useUpdateStatusMutation() + const theme = useTheme() + const dispatch = useDispatch() + + 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_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, + status : event.status + }) + const [expanded, setExpanded] = React.useState(false) + const handleExpandClick = (e) => { + e.stopPropagation() + setExpanded(!expanded); + }; + + const { data : user } = useGetUserByIdQuery(event.user_id) + + const userName = `${user?.first_name || ""} ${user?.last_name || ""}` + const userAvatarUrl = user?.avatar_url || "" + + const longDescription = event?.metadata?.error?.LongDescription || [] + const probableCause = event?.metadata?.error?.ProbableCause || [] + const suggestedRemediation = event?.metadata?.error?.SuggestedRemediation || [] + + + return ( + +
+ + + + + + {event.description} + + + + {formatTimestamp(event.created_at)} + + + + + + + + + + + + + + {event.user_id && + + + + } + {event.system_id && + + + + } + + + + + +
+ +
+ +
+
+ + 0 ? 6 : 12}> + + + 0 ? 6 : 12} > + + + +
+
+
+
+
+
+ ) + +} +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/components/NotificationCenter/notificationCenter.style.js b/ui/components/NotificationCenter/notificationCenter.style.js new file mode 100644 index 00000000000..1504b758649 --- /dev/null +++ b/ui/components/NotificationCenter/notificationCenter.style.js @@ -0,0 +1,88 @@ +import { makeStyles } from "@material-ui/core" + +export const useStyles = makeStyles((theme) => ({ + sidelist : { + width : "45rem", + maxWidth : "95vw", + }, + 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 : "1.25rem" + }, + 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 : "2.25rem", + height : "2.25rem", + borderRadius : "100%", + backgroundColor : "black", + display : "flex", + padding : "0.2rem", + justifyContent : "center", + alignItems : "center", + cursor : "pointer" + }, + severityChip : { + borderRadius : "0.25rem", + display : "flex", + gap : "0.25rem", + justifyContent : "start", + alignItems : "center", + fontSize : "1rem", + cursor : "pointer", + }, + + severityChips : { + display : "flex", + gap : "0.75rem", + alignItems : "center", + + }, + notification : { + margin : theme.spacing(0.5, 1), + }, +})); + + +export const useNavNotificationIconStyles = makeStyles(() => ({ + root : (props) => ({ + '& .MuiBadge-badge' : { + backgroundColor : props.badgeColor, + }, + }), +})) diff --git a/ui/components/PromptComponent.js b/ui/components/PromptComponent.js index c0d0cc472ec..fc107b1f558 100644 --- a/ui/components/PromptComponent.js +++ b/ui/components/PromptComponent.js @@ -7,7 +7,9 @@ import { DialogActions, DialogContentText, DialogContent, - DialogTitle + DialogTitle, + FormControlLabel, + Checkbox } from "@material-ui/core"; import classNames from 'classnames'; @@ -23,7 +25,8 @@ const styles = (theme) => ({ minWidth : 400, overflowWrap : 'anywhere', textAlign : 'center', - padding : '5px' + padding : '5px', + color : theme.palette.secondary.text }, actions : { display : 'flex', @@ -57,6 +60,15 @@ const styles = (theme) => ({ "&:hover" : { backgroundColor : "#B32700", } + }, + checkboxLabelStyle : { + fontSize : "1rem", + }, + checkbox : { + color : theme.palette.secondary.focused, + "&$checked" : { + color : theme.palette.secondary.focused, + } } }); @@ -67,7 +79,9 @@ class PromptComponent extends React.Component { show : false, title : "", subtitle : "", - options : [] + options : [], + isChecked : false, + showCheckbox : false } this.promiseInfo = {}; } @@ -80,6 +94,7 @@ class PromptComponent extends React.Component { title : passed.title, subtitle : passed.subtitle, options : passed.options, + showCheckbox : !!passed.showCheckbox, show : true }); }); @@ -89,9 +104,20 @@ class PromptComponent extends React.Component { this.setState({ show : false }); }; + handleCheckboxChange = () => { + this.setState((prevState) => ({ + isChecked : !prevState.isChecked, + })); + }; + + getCheckboxState = () => { + return this.state.isChecked; + }; + + render() { const { - show, options, title, subtitle + show, options, title, subtitle, isChecked, showCheckbox } = this.state; const { classes } = this.props; const { resolve } = this.promiseInfo; @@ -116,6 +142,23 @@ class PromptComponent extends React.Component { {subtitle} + {showCheckbox && ( + + } + label={ + + Do not show again + + } + /> + )} } 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/components/User.js b/ui/components/User.js index c8e9cd2188a..8281012d2fa 100644 --- a/ui/components/User.js +++ b/ui/components/User.js @@ -244,4 +244,4 @@ const mapStateToProps = state => ({ export default withStyles(styles)(connect( mapStateToProps, mapDispatchToProps, -)(withNotify((withRouter(User))))); +)(withNotify((withRouter(User))))); \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 71753de70bb..5e04c675662 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": "latest", + "jss": "*", "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": "latest", + "prop-types": "*", "react": "^17.0.2", "react-big-calendar": "^0.35.0", "react-codemirror2": "^7.2.1", @@ -102,7 +103,7 @@ "@storybook/testing-library": "^0.0.13", "ajv": "^8.12.0", "babel-loader": "^9.1.2", - "cypress": "^12.0.2", + "cypress": "^13.2.0", "cypress-file-upload": "^5.0.8", "eslint": "^8.35.0", "eslint-config-next": "^13.1.2", @@ -2229,9 +2230,9 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.12", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", - "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -2247,7 +2248,7 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.10.3", + "qs": "6.10.4", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -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", @@ -11759,15 +11783,15 @@ "dev": true }, "node_modules/cypress": { - "version": "12.17.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.1.tgz", - "integrity": "sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.2.0.tgz", + "integrity": "sha512-AvDQxBydE771GTq0TR4ZUBvv9m9ffXuB/ueEtpDF/6gOcvFR96amgwSJP16Yhqw6VhmwqspT5nAGzoxxB+D89g==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^2.88.11", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -11800,6 +11824,7 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "semver": "^7.5.3", @@ -11812,7 +11837,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^14.0.0 || ^16.0.0 || >=18.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, "node_modules/cypress-file-upload": { @@ -11827,6 +11852,12 @@ "cypress": ">3.0.0" } }, + "node_modules/cypress/node_modules/@types/node": { + "version": "18.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.16.tgz", + "integrity": "sha512-e0zgs7qe1XH/X3KEPnldfkD07LH9O1B9T31U8qoO7lqGSjj3/IrBuvqMeJ1aYejXRK3KOphIUDw6pLIplEW17A==", + "dev": true + }, "node_modules/cypress/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -15929,6 +15960,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 +22173,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 +22427,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", @@ -27723,9 +27768,9 @@ } }, "@cypress/request": { - "version": "2.88.12", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz", - "integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -27741,7 +27786,7 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.10.3", + "qs": "6.10.4", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -29295,6 +29340,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", @@ -34807,14 +34863,14 @@ "dev": true }, "cypress": { - "version": "12.17.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.1.tgz", - "integrity": "sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.2.0.tgz", + "integrity": "sha512-AvDQxBydE771GTq0TR4ZUBvv9m9ffXuB/ueEtpDF/6gOcvFR96amgwSJP16Yhqw6VhmwqspT5nAGzoxxB+D89g==", "dev": true, "requires": { - "@cypress/request": "^2.88.11", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -34847,6 +34903,7 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "semver": "^7.5.3", @@ -34856,6 +34913,12 @@ "yauzl": "^2.10.0" }, "dependencies": { + "@types/node": { + "version": "18.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.16.tgz", + "integrity": "sha512-e0zgs7qe1XH/X3KEPnldfkD07LH9O1B9T31U8qoO7lqGSjj3/IrBuvqMeJ1aYejXRK3KOphIUDw6pLIplEW17A==", + "dev": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -38000,6 +38063,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 +42778,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 +42986,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..0b56227c09a 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", @@ -130,7 +131,7 @@ "@storybook/testing-library": "^0.0.13", "ajv": "^8.12.0", "babel-loader": "^9.1.2", - "cypress": "^12.0.2", + "cypress": "^13.2.0", "cypress-file-upload": "^5.0.8", "eslint": "^8.35.0", "eslint-config-next": "^13.1.2", diff --git a/ui/pages/_app.js b/ui/pages/_app.js index 9ea761cedaf..fd59dfab304 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'; @@ -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'; @@ -53,6 +53,13 @@ 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'; +import { api as mesheryApi } from "../rtk-query" +import { PROVIDER_TAGS } from '../rtk-query/notificationCenter'; +import { useNotification } from '../utils/hooks/useNotification'; + + if (typeof window !== 'undefined') { require('codemirror/mode/yaml/yaml'); @@ -69,6 +76,40 @@ 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, + system_id : result.event.systemID, + 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)}`) } @@ -89,7 +130,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, @@ -101,11 +141,12 @@ class MesheryApp extends App { disposeK8sContextSubscription : null, theme : 'light', isOpen : false, - relayEnvironment : createRelayEnvironment(), + relayEnvironment : createRelayEnvironment() }; } - initMeshSyncEventsSubscription(contexts=[]) { + + initMeshSyncEventsSubscription(contexts = []) { if (this.meshsyncEventsSubscriptionRef.current) { this.meshsyncEventsSubscriptionRef.current.dispose(); } @@ -168,10 +209,10 @@ class MesheryApp extends App { ) this.initMeshSyncEventsSubscription(this.state.activeK8sContexts); - this.initEventsSubscription() - const k8sContextSubscription = (page="", search="", pageSize="10", order="") => { + // this.initEventsSubscription() + const k8sContextSubscription = (page = "", search = "", pageSize = "10", order = "") => { return subscribeK8sContext((result) => { - this.setState({ k8sContexts : result.k8sContext }, () => this.setActiveContexts("all")) + this.setState({ k8sContexts : result.k8sContext }, () => this.setActiveContexts("all")) this.props.store.dispatch({ type : actionTypes.UPDATE_CLUSTER_CONFIG, k8sConfig : result.k8sContext.contexts }); }, { @@ -193,16 +234,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: ", result); - }) - 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; @@ -426,8 +480,10 @@ class MesheryApp extends App { }} maxSnack={10} > + + - {!this.state.isFullScreenMode &&
-