diff --git a/package-lock.json b/package-lock.json index 75af281b..1242f16e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,15 @@ "@nivo/bar": "^0.79.1", "@nivo/core": "^0.79.0", "@nivo/network": "^0.79.1", + "@types/dagre": "^0.7.47", + "dagre": "^0.8.5", "dayjs": "^1.11.0", "i18next": "^21.6.14", "mdi-react": "^8.2.0", "react": "^17.0.2", "react-copy-to-clipboard": "^5.0.4", "react-dom": "^17.0.2", + "react-flow-renderer": "^10.0.8", "react-i18next": "^11.15.4", "react-infinite-scroll-component": "^6.1.0", "react-jazzicon": "^1.0.3", @@ -3985,6 +3988,11 @@ "@types/node": "*" } }, + "node_modules/@types/dagre": { + "version": "0.7.47", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.47.tgz", + "integrity": "sha512-oX+3aRf7L6Cqq1MvbWmmD7FpAU/T8URwFFuHBagAiyHILn3i+RNZ35/tvyq28de+lZGY3W19BxJ7FeITQDO7aA==" + }, "node_modules/@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -5788,6 +5796,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" }, + "node_modules/classcat": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.3.tgz", + "integrity": "sha512-6dK2ke4VEJZOFx2ZfdDAl5OhEL8lvkl6EHF92IfRePfHxQTqir5NlcNVUv+2idjDqCX2NDc8m8YSAI5NI975ZQ==" + }, "node_modules/clean-css": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz", @@ -6557,6 +6570,26 @@ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==" }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", @@ -6619,6 +6652,14 @@ "d3-array": "2" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", @@ -6645,6 +6686,48 @@ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==" }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8756,6 +8839,14 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -14402,11 +14493,44 @@ "react": "17.0.2" } }, + "node_modules/react-draggable": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.4.tgz", + "integrity": "sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-overlay": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==" }, + "node_modules/react-flow-renderer": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-10.0.8.tgz", + "integrity": "sha512-FYUZcgdsBUAdelZLZ1mj19bSRJkGsM03B+xUMFWPppwndLV/onRh4G1b8jjRjiQ90ZNIiSI44yCQ25+yg4dAjA==", + "dependencies": { + "@babel/runtime": "^7.17.8", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "react-draggable": "^4.4.4", + "zustand": "^3.7.1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "16 || 17", + "react-dom": "16 || 17" + } + }, "node_modules/react-i18next": { "version": "11.16.2", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.2.tgz", @@ -17536,6 +17660,22 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } } }, "dependencies": { @@ -20163,6 +20303,11 @@ "@types/node": "*" } }, + "@types/dagre": { + "version": "0.7.47", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.47.tgz", + "integrity": "sha512-oX+3aRf7L6Cqq1MvbWmmD7FpAU/T8URwFFuHBagAiyHILn3i+RNZ35/tvyq28de+lZGY3W19BxJ7FeITQDO7aA==" + }, "@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -21565,6 +21710,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==" }, + "classcat": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.3.tgz", + "integrity": "sha512-6dK2ke4VEJZOFx2ZfdDAl5OhEL8lvkl6EHF92IfRePfHxQTqir5NlcNVUv+2idjDqCX2NDc8m8YSAI5NI975ZQ==" + }, "clean-css": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz", @@ -22124,6 +22274,20 @@ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==" }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, "d3-force": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", @@ -22188,6 +22352,11 @@ "d3-interpolate": "1 - 2" } }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, "d3-shape": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", @@ -22214,6 +22383,39 @@ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==" }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -23758,6 +23960,14 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "^4.17.15" + } + }, "gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -27727,11 +27937,33 @@ "scheduler": "^0.20.2" } }, + "react-draggable": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.4.tgz", + "integrity": "sha512-6e0WdcNLwpBx/YIDpoyd2Xb04PB0elrDrulKUgdrIlwuYvxh5Ok9M+F8cljm8kPXXs43PmMzek9RrB1b7mLMqA==", + "requires": { + "clsx": "^1.1.1", + "prop-types": "^15.6.0" + } + }, "react-error-overlay": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==" }, + "react-flow-renderer": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-10.0.8.tgz", + "integrity": "sha512-FYUZcgdsBUAdelZLZ1mj19bSRJkGsM03B+xUMFWPppwndLV/onRh4G1b8jjRjiQ90ZNIiSI44yCQ25+yg4dAjA==", + "requires": { + "@babel/runtime": "^7.17.8", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "react-draggable": "^4.4.4", + "zustand": "^3.7.1" + } + }, "react-i18next": { "version": "11.16.2", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.2.tgz", @@ -30066,6 +30298,12 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "requires": {} } } } diff --git a/package.json b/package.json index fdd950c9..8aed51ce 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,15 @@ "@nivo/bar": "^0.79.1", "@nivo/core": "^0.79.0", "@nivo/network": "^0.79.1", + "@types/dagre": "^0.7.47", + "dagre": "^0.8.5", "dayjs": "^1.11.0", "i18next": "^21.6.14", "mdi-react": "^8.2.0", "react": "^17.0.2", "react-copy-to-clipboard": "^5.0.4", "react-dom": "^17.0.2", + "react-flow-renderer": "^10.0.8", "react-i18next": "^11.15.4", "react-infinite-scroll-component": "^6.1.0", "react-jazzicon": "^1.0.3", diff --git a/src/components/Cards/FireFlyCard.tsx b/src/components/Cards/FireFlyCard.tsx index fdf13cf8..e7d7ef60 100644 --- a/src/components/Cards/FireFlyCard.tsx +++ b/src/components/Cards/FireFlyCard.tsx @@ -1,4 +1,5 @@ -import { Grid, Typography } from '@mui/material'; +import { ArrowForward } from '@mui/icons-material'; +import { Grid, IconButton, Typography } from '@mui/material'; import { Box } from '@mui/system'; import { IFireFlyCard } from '../../interfaces'; import { DEFAULT_BORDER_RADIUS } from '../../theme'; @@ -35,7 +36,27 @@ export const FireFlyCard: React.FC = ({ {card.headerText} - {card.headerComponent} + { + + {card.headerComponent ? ( + card.headerComponent + ) : ( + // Fake icon button + + + + )} + + } { + if ( + children.data.applications?.length && + Object.keys(children.data.plugins)?.length + ) { + return ( + <> + + + {children.data.label} + + + + {children.data.subtitle} + + + + {children.data.applications.map((_, idx) => { + return ( + + ); + })} + {Object.keys(children.data.plugins).map((_, idx) => { + return ( + + ); + })} + + ); + } else { + return <>; + } +}); diff --git a/src/components/Charts/MyNodeDiagram.tsx b/src/components/Charts/MyNodeDiagram.tsx new file mode 100644 index 00000000..acde5301 --- /dev/null +++ b/src/components/Charts/MyNodeDiagram.tsx @@ -0,0 +1,271 @@ +import { Typography } from '@mui/material'; +import { Box } from '@mui/system'; +import dagre from 'dagre'; +import i18next from 'i18next'; +import React, { useContext, useEffect, useState } from 'react'; +import ReactFlow, { + Edge, + MarkerType, + Node, + NodeTypes, + Position, + ReactFlowProvider, + useEdgesState, + useNodesState, +} from 'react-flow-renderer'; +import { ApplicationContext } from '../../contexts/ApplicationContext'; +import { IStatus, IWebsocketConnection } from '../../interfaces'; +import { DEFAULT_BORDER_RADIUS, FFColors } from '../../theme'; +import { FFCircleLoader } from '../Loaders/FFCircleLoader'; +import { DiagramFireFlyNode } from './DiagramFireFlyNode'; + +export const HANDLE_PREFIX = 'handle_'; +export const APP_PREFIX = 'app_'; +export const FF_NODE_PREFIX = 'firefly_'; +export const PLUGIN_PREFIX = 'plugin_'; +export const position = { x: 0, y: 0 }; + +const FF_WIDTH = 100; +const FF_HEIGHT = 400; + +const nodeStyle = { + border: `3px solid ${FFColors.Yellow}`, + borderRadius: DEFAULT_BORDER_RADIUS, + color: '#FFF', + background: 'transparent', + fontSize: '14px', +}; + +const edgeStyle = { + color: FFColors.Orange, + stroke: FFColors.Orange, + strokeWidth: '3', +}; + +const dagreGraph = new dagre.graphlib.Graph(); +dagreGraph.setDefaultEdgeLabel(() => ({})); + +const nodeWidth = 350; +const nodeHeight = 25; + +const getLayoutedElements = (nodes: Node[], edges: Edge[]) => { + dagreGraph.setGraph({ rankdir: 'LR' }); + + nodes.forEach((node) => { + if (node.id !== FF_NODE_PREFIX) { + dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + } else { + dagreGraph.setNode(node.id, { width: FF_WIDTH, height: FF_HEIGHT }); + } + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + nodes.forEach((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + node.targetPosition = Position.Left; + node.sourcePosition = Position.Right; + + // We are shifting the dagre node position (anchor=center center) to the top left + // so it matches the React Flow node anchor point (top left). + node.position = { + x: nodeWithPosition.x - nodeWidth / 2, + y: + nodeWithPosition.y - + (node.id !== FF_NODE_PREFIX ? nodeHeight : FF_HEIGHT) / 2, + }; + + return node; + }); + + return { nodes, edges }; +}; + +const makeInitialNodes = ( + applications: any[], + plugins: IStatus['plugins'], + nodeName: string +) => { + let nodes: Node[] = []; + // Applications (Left side) + nodes = nodes.concat( + applications.map((a, idx) => { + return { + id: `${APP_PREFIX}${idx}`, + sourcePosition: Position.Right, + type: 'input', + style: nodeStyle, + data: { label: a.remoteAddress }, + position, + }; + }) + ); + // Firefly + nodes.push({ + type: 'fireflyNode', + id: FF_NODE_PREFIX, + targetPosition: Position.Left, + sourcePosition: Position.Right, + style: { ...nodeStyle, height: FF_HEIGHT, width: FF_WIDTH }, + data: { + applications, + plugins, + label: i18next.t('firefly'), + subtitle: nodeName, + }, + position, + }); + // Plugins (Right side) + Object.entries(plugins).map(([k, v]) => { + nodes = nodes.concat( + v.map((plugin, idx) => { + return { + id: `${PLUGIN_PREFIX}${k}_${idx}`, + targetPosition: Position.Left, + type: 'output', + data: { label: plugin.connection }, + position, + style: nodeStyle, + }; + }) + ); + }); + + return nodes; +}; + +const makeInitialEdges = ( + applications: IWebsocketConnection[], + plugins: IStatus['plugins'] +) => { + let edges: Edge[] = []; + + // Apps + edges = edges.concat( + applications.map((_, idx): Edge => { + return { + id: `${APP_PREFIX}${FF_NODE_PREFIX}${idx}`, + source: `${APP_PREFIX}${idx}`, + targetHandle: `${HANDLE_PREFIX}${APP_PREFIX}${idx}`, + target: FF_NODE_PREFIX, + style: edgeStyle, + markerEnd: { + type: MarkerType.ArrowClosed, + color: FFColors.Orange, + }, + label: i18next.t('websocket'), + labelBgPadding: [8, 4], + labelBgBorderRadius: 4, + labelBgStyle: { + fill: '#1e242a', + fillOpacity: 0.97, + }, + labelStyle: { fill: '#FFFFFF', fontWeight: 700 }, + }; + }) + ); + // Plugins + Object.entries(plugins).map(([k, v], pIdx) => { + edges = edges.concat( + v.map((_, idx) => { + return { + id: `${PLUGIN_PREFIX}${FF_NODE_PREFIX}${k}_${idx}`, + source: FF_NODE_PREFIX, + sourceHandle: `${HANDLE_PREFIX}${PLUGIN_PREFIX}${pIdx}`, + target: `${PLUGIN_PREFIX}${k}_${idx}`, + style: edgeStyle, + markerEnd: { + type: MarkerType.ArrowClosed, + color: FFColors.Orange, + }, + label: i18next.t(k), + labelBgPadding: [8, 4], + labelBgBorderRadius: 4, + labelBgStyle: { + fill: '#1e242a', + fillOpacity: 0.97, + }, + labelStyle: { fill: '#FFFFFF', fontWeight: 700 }, + }; + }) + ); + }); + + return edges; +}; + +const nodeTypes: NodeTypes = { + fireflyNode: DiagramFireFlyNode, +}; + +interface Props { + applications: IWebsocketConnection[]; + plugins: IStatus['plugins']; +} + +export const MyNodeDiagram: React.FC = ({ applications, plugins }) => { + const { nodeName } = useContext(ApplicationContext); + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + return () => { + setIsMounted(false); + }; + }, []); + + const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( + makeInitialNodes(applications, plugins, nodeName), + makeInitialEdges(applications, plugins) + ); + + const [nodes, , onNodesChange] = useNodesState(layoutedNodes); + const [edges, , onEdgesChange] = useEdgesState(layoutedEdges); + + if ( + !plugins || + !applications || + applications.length === 0 || + Object.keys(plugins ?? {}).length === 0 + ) { + return ( + + + + ); + } else { + return ( + + + {isMounted && ( + + )} + + done + + ); + } +}; diff --git a/src/components/Lists/BalanceList.tsx b/src/components/Lists/BalanceList.tsx index f70eed4c..9ddc538d 100644 --- a/src/components/Lists/BalanceList.tsx +++ b/src/components/Lists/BalanceList.tsx @@ -7,7 +7,6 @@ import { ITokenBalance, ITokenPool } from '../../interfaces'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; import { PoolButton } from '../Buttons/PoolButton'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -64,15 +63,9 @@ export const BalanceList: React.FC = ({ balance, pool }) => { return ( <> - {!balance ? ( - - ) : ( - <> - {dataList.map((d, idx) => ( - - ))} - - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/BlockchainEventList.tsx b/src/components/Lists/BlockchainEventList.tsx index 2a2e8b66..d754d342 100644 --- a/src/components/Lists/BlockchainEventList.tsx +++ b/src/components/Lists/BlockchainEventList.tsx @@ -5,7 +5,6 @@ import { FF_TX_CATEGORY_MAP, IBlockchainEvent } from '../../interfaces'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; import { TxButton } from '../Buttons/TxButton'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -61,14 +60,8 @@ export const BlockchainEventList: React.FC = ({ be }) => { return ( <> - {!be ? ( - - ) : ( - <> - {dataList.map( - (d, idx) => d.label !== '' && - )} - + {dataList.map( + (d, idx) => d.label !== '' && )} ); diff --git a/src/components/Lists/DataList.tsx b/src/components/Lists/DataList.tsx index 36146d0e..019379c9 100644 --- a/src/components/Lists/DataList.tsx +++ b/src/components/Lists/DataList.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { IData } from '../../interfaces'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -44,15 +43,9 @@ export const DataList: React.FC = ({ data }) => { return ( <> - {!data ? ( - - ) : ( - <> - {dataList.map((d, idx) => ( - - ))} - - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/DatatypeList.tsx b/src/components/Lists/DatatypeList.tsx index 032a7e51..d5dc85f7 100644 --- a/src/components/Lists/DatatypeList.tsx +++ b/src/components/Lists/DatatypeList.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { IDatatype } from '../../interfaces'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -53,15 +52,9 @@ export const DatatypeList: React.FC = ({ dt }) => { return ( <> - {!dt ? ( - - ) : ( - <> - {dataList.map((d, idx) => ( - - ))} - - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/InterfaceList.tsx b/src/components/Lists/InterfaceList.tsx index d838d7ca..6c260427 100644 --- a/src/components/Lists/InterfaceList.tsx +++ b/src/components/Lists/InterfaceList.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { IContractInterface } from '../../interfaces'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFSkeletonList } from './FFSkeletonList'; @@ -54,15 +53,9 @@ export const InterfaceList: React.FC = ({ cInterface }) => { return ( <> - {!cInterface ? ( - - ) : ( - <> - {dataList.map((d, idx) => ( - - ))} - - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/ListenerList.tsx b/src/components/Lists/ListenerList.tsx index fc17f5e5..658f8319 100644 --- a/src/components/Lists/ListenerList.tsx +++ b/src/components/Lists/ListenerList.tsx @@ -5,7 +5,6 @@ import { IContractListener } from '../../interfaces'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; import { InterfaceButton } from '../Buttons/InterfaceButton'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -78,15 +77,9 @@ export const ListenerList: React.FC = ({ listener }) => { return ( <> - {!listener ? ( - - ) : ( - <> - {dataList.map((d, idx) => ( - - ))} - - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/MessageList.tsx b/src/components/Lists/MessageList.tsx index f72babfb..51770a76 100644 --- a/src/components/Lists/MessageList.tsx +++ b/src/components/Lists/MessageList.tsx @@ -1,11 +1,12 @@ -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { IMessage } from '../../interfaces'; +import { ApplicationContext } from '../../contexts/ApplicationContext'; +import { FF_NAV_PATHS, IMessage } from '../../interfaces'; import { FF_TX_CATEGORY_MAP } from '../../interfaces/enums/transactionTypes'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; +import { LaunchButton } from '../Buttons/LaunchButton'; import { MsgStatusChip } from '../Chips/MsgStatusChip'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -16,6 +17,7 @@ interface Props { } export const MessageList: React.FC = ({ message }) => { + const { selectedNamespace } = useContext(ApplicationContext); const { t } = useTranslation(); const [dataList, setDataList] = useState(FFSkeletonList); @@ -72,6 +74,21 @@ export const MessageList: React.FC = ({ message }) => { ) : undefined, }, + { + label: t('batchID'), + value: , + button: ( + <> + + + + ), + }, { label: t('status'), value: message && , @@ -86,15 +103,9 @@ export const MessageList: React.FC = ({ message }) => { return ( <> - {!message ? ( - - ) : ( - <> - {dataList.map((d, idx) => ( - - ))} - - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/OperationList.tsx b/src/components/Lists/OperationList.tsx index 66093ad6..448a9c70 100644 --- a/src/components/Lists/OperationList.tsx +++ b/src/components/Lists/OperationList.tsx @@ -7,7 +7,6 @@ import { FFCopyButton } from '../Buttons/CopyButton'; import { OpRetryButton } from '../Buttons/OpRetryButton'; import { TxButton } from '../Buttons/TxButton'; import { OpStatusChip } from '../Chips/OpStatusChip'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -71,14 +70,8 @@ export const OperationList: React.FC = ({ op, showTxLink = true }) => { return ( <> - {!op ? ( - - ) : ( - <> - {dataList.map( - (d, idx) => d.label !== '' && - )} - + {dataList.map( + (d, idx) => d.label !== '' && )} ); diff --git a/src/components/Lists/PoolList.tsx b/src/components/Lists/PoolList.tsx index 9a2cd440..db318dc0 100644 --- a/src/components/Lists/PoolList.tsx +++ b/src/components/Lists/PoolList.tsx @@ -7,7 +7,6 @@ import { FFCopyButton } from '../Buttons/CopyButton'; import { MsgButton } from '../Buttons/MsgButton'; import { TxButton } from '../Buttons/TxButton'; import { PoolStatusChip } from '../Chips/PoolStatusChip'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -30,6 +29,23 @@ export const PoolList: React.FC = ({ pool }) => { value: , button: , }, + { + label: t('standard'), + value: ( + <> + + + ), + button: , + }, + { + label: t('connector'), + value: , + button: , + }, { label: t('transactionID'), value: pool.tx?.id ? ( @@ -75,15 +91,9 @@ export const PoolList: React.FC = ({ pool }) => { return ( <> - {!pool ? ( - - ) : ( - <> - {dataList.map((d, idx) => ( - - ))} - - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/SubList.tsx b/src/components/Lists/SubList.tsx index 4078226f..7db5b0ba 100644 --- a/src/components/Lists/SubList.tsx +++ b/src/components/Lists/SubList.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { ISubscription } from '../../interfaces'; import { IDataListItem } from '../../interfaces/lists'; import { FFCopyButton } from '../Buttons/CopyButton'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -39,11 +38,9 @@ export const SubList: React.FC = ({ sub }) => { return ( <> - {!sub ? ( - - ) : ( - dataList.map((d, idx) => ) - )} + {dataList.map((d, idx) => ( + + ))} ); }; diff --git a/src/components/Lists/SubOptionsList.tsx b/src/components/Lists/SubOptionsList.tsx new file mode 100644 index 00000000..e5d91fb3 --- /dev/null +++ b/src/components/Lists/SubOptionsList.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ISubscription } from '../../interfaces'; +import { IDataListItem } from '../../interfaces/lists'; +import { FFCopyButton } from '../Buttons/CopyButton'; +import { FFListItem } from './FFListItem'; +import { FFListText } from './FFListText'; +import { FFSkeletonList } from './FFSkeletonList'; + +interface Props { + options?: ISubscription['options']; +} + +export const SubOptionsList: React.FC = ({ options }) => { + const { t } = useTranslation(); + const [dataList, setDataList] = useState(FFSkeletonList); + + useEffect(() => { + if (options) { + setDataList([ + { + label: t('firstEvent'), + value: , + button: , + }, + { + label: t('readAhead'), + value: ( + + ), + button: , + }, + { + label: t('withData'), + value: ( + + ), + button: ( + + ), + }, + ]); + } + }, [options]); + + return ( + <> + {dataList.map((d, idx) => ( + + ))} + + ); +}; diff --git a/src/components/Lists/TransferList.tsx b/src/components/Lists/TransferList.tsx index e2a44934..0a9ca7d5 100644 --- a/src/components/Lists/TransferList.tsx +++ b/src/components/Lists/TransferList.tsx @@ -8,7 +8,6 @@ import { MsgButton } from '../Buttons/MsgButton'; import { PoolButton } from '../Buttons/PoolButton'; import { TxButton } from '../Buttons/TxButton'; import { TxStatusChip } from '../Chips/TxStatusChip'; -import { FFCircleLoader } from '../Loaders/FFCircleLoader'; import { FFListItem } from './FFListItem'; import { FFListText } from './FFListText'; import { FFListTimestamp } from './FFListTimestamp'; @@ -130,14 +129,8 @@ export const TransferList: React.FC = ({ return ( <> - {!transfer ? ( - - ) : ( - <> - {dataList.map( - (d, idx) => d.label !== '' && - )} - + {dataList.map( + (d, idx) => d.label !== '' && )} ); diff --git a/src/components/Lists/WebsocketList.tsx b/src/components/Lists/WebsocketList.tsx new file mode 100644 index 00000000..3d9c3891 --- /dev/null +++ b/src/components/Lists/WebsocketList.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IWebsocketConnection } from '../../interfaces'; +import { IDataListItem } from '../../interfaces/lists'; +import { FFCopyButton } from '../Buttons/CopyButton'; +import { FFListItem } from './FFListItem'; +import { FFListText } from './FFListText'; +import { FFSkeletonList } from './FFSkeletonList'; + +interface Props { + ws?: IWebsocketConnection; +} + +export const WebsocketList: React.FC = ({ ws }) => { + const { t } = useTranslation(); + const [dataList, setDataList] = useState(FFSkeletonList); + + useEffect(() => { + if (ws) { + setDataList([ + { + label: t('id'), + value: , + button: , + }, + { + label: t('remoteAddress'), + value: , + button: , + }, + { + label: t('userAgent'), + value: ws.userAgent.length ? ( + + ) : ( + + ), + button: ws.userAgent.length ? : <>, + }, + { + label: t('id'), + value: , + button: , + }, + ]); + } + }, [ws]); + + return ( + <> + {dataList.map((d, idx) => ( + + ))} + + ); +}; diff --git a/src/components/Navigation/MyNodeNav.tsx b/src/components/Navigation/MyNodeNav.tsx index fa345489..9e9d0070 100644 --- a/src/components/Navigation/MyNodeNav.tsx +++ b/src/components/Navigation/MyNodeNav.tsx @@ -31,6 +31,8 @@ export const MyNodeNav = () => { const myNodePath = FF_NAV_PATHS.myNodePath(selectedNamespace); const myNodeSubscriptionsPath = FF_NAV_PATHS.myNodeSubscriptionsPath(selectedNamespace); + const myNodeWebsocketsPath = + FF_NAV_PATHS.myNodeWebsocketsPath(selectedNamespace); const navItems: INavItem[] = [ { @@ -43,6 +45,11 @@ export const MyNodeNav = () => { action: () => navigate(myNodeSubscriptionsPath), itemIsActive: pathname === myNodeSubscriptionsPath, }, + { + name: t('websockets'), + action: () => navigate(myNodeWebsocketsPath), + itemIsActive: pathname === myNodeWebsocketsPath, + }, ]; return ( diff --git a/src/components/Slides/SubscriptionSlide.tsx b/src/components/Slides/SubscriptionSlide.tsx index b3c4be6e..0a402fdd 100644 --- a/src/components/Slides/SubscriptionSlide.tsx +++ b/src/components/Slides/SubscriptionSlide.tsx @@ -21,8 +21,10 @@ import { ISubscription } from '../../interfaces'; import { DEFAULT_PADDING } from '../../theme'; import { JsonViewAccordion } from '../Accordions/JsonViewerAccordion'; import { SubList } from '../Lists/SubList'; +import { SubOptionsList } from '../Lists/SubOptionsList'; import { DisplaySlide } from './DisplaySlide'; import { SlideHeader } from './SlideHeader'; +import { SlideSectionHeader } from './SlideSectionHeader'; interface Props { sub: ISubscription; @@ -40,9 +42,14 @@ export const SubscriptionSlide: React.FC = ({ sub, open, onClose }) => { {/* Header */} {/* Data list */} - + + {/* Filter options */} + + + + {sub.filter && ( @@ -53,15 +60,6 @@ export const SubscriptionSlide: React.FC = ({ sub, open, onClose }) => { /> )} - {sub.options && ( - - - - )} diff --git a/src/components/Slides/TransactionSlide.tsx b/src/components/Slides/TransactionSlide.tsx index 74556efa..9e4aa173 100644 --- a/src/components/Slides/TransactionSlide.tsx +++ b/src/components/Slides/TransactionSlide.tsx @@ -140,8 +140,8 @@ export const TransactionSlide: React.FC = ({ {txOperations?.map((op, idx) => ( - - + + ))} diff --git a/src/components/Slides/WebsocketSlide.tsx b/src/components/Slides/WebsocketSlide.tsx new file mode 100644 index 00000000..702e476c --- /dev/null +++ b/src/components/Slides/WebsocketSlide.tsx @@ -0,0 +1,57 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Grid } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { IWebsocketConnection } from '../../interfaces'; +import { DEFAULT_PADDING } from '../../theme'; +import { JsonViewAccordion } from '../Accordions/JsonViewerAccordion'; +import { WebsocketList } from '../Lists/WebsocketList'; +import { DisplaySlide } from './DisplaySlide'; +import { SlideHeader } from './SlideHeader'; + +interface Props { + ws: IWebsocketConnection; + open: boolean; + onClose: () => void; +} + +export const WebsocketSlide: React.FC = ({ ws, open, onClose }) => { + const { t } = useTranslation(); + + return ( + <> + + + {/* Header */} + + {/* Data list */} + + + + + + + + + + ); +}; diff --git a/src/components/Tables/Table.tsx b/src/components/Tables/Table.tsx index c90a99ca..e5e334c1 100644 --- a/src/components/Tables/Table.tsx +++ b/src/components/Tables/Table.tsx @@ -153,6 +153,17 @@ export const DataTable: React.FC = ({ /> ); })} + {records && + rowsPerPage && + Array.from(Array(rowsPerPage - records?.length)).map( + (_, idx) => ( + + ) + )} diff --git a/src/components/Tables/TableEmptyState.tsx b/src/components/Tables/TableEmptyState.tsx index a4c0eceb..fcd8a344 100644 --- a/src/components/Tables/TableEmptyState.tsx +++ b/src/components/Tables/TableEmptyState.tsx @@ -42,7 +42,7 @@ export const DataTableEmptyState: React.FC = ({ }) => { return ( <> - + @@ -70,7 +70,14 @@ export const DataTableEmptyState: React.FC = ({
- + = ({ numColumns }) => { +export const TableRowSkeleton: React.FC = ({ + numColumns, + noSkeleton = false, +}) => { return ( = ({ numColumns }) => { borderBottomColor: 'background.default', }} > - + {!noSkeleton && } ); })} diff --git a/src/index.css b/src/index.css index 178f4244..38facd04 100644 --- a/src/index.css +++ b/src/index.css @@ -19,3 +19,8 @@ pre { white-space: -o-pre-wrap; word-wrap: break-word; } + +/* Remove react flow watermark */ +.react-flow__attribution { + display: none; +} diff --git a/src/interfaces/api.ts b/src/interfaces/api.ts index 3dab0ed2..0032bf61 100644 --- a/src/interfaces/api.ts +++ b/src/interfaces/api.ts @@ -456,6 +456,18 @@ export interface IStatus { defaults: { namespace: string; }; + plugins: { + blockchain: IStatusPluginDetails[]; + database: IStatusPluginDetails[]; + dataExchange: IStatusPluginDetails[]; + identity: IStatusPluginDetails[]; + sharedStorage: IStatusPluginDetails[]; + tokens: IStatusPluginDetails[]; + }; +} + +export interface IStatusPluginDetails { + connection: string; } export interface ISubscription { @@ -464,7 +476,11 @@ export interface ISubscription { name: string; transport: string; filter?: any; - options?: any; + options: { + firstEvent: string; + readAhead: number; + withData: boolean; + }; created: string; updated: string | null; } @@ -574,3 +590,21 @@ export interface IVerifier { type: string; value: string; } + +export interface IWebsocketStatus { + enabled: boolean; + connections: IWebsocketConnection[]; +} + +export interface IWebsocketConnection { + id: string; + remoteAddress: string; + userAgent: string; + subscriptions: IWebsocketSubscriptions[]; +} + +export interface IWebsocketSubscriptions { + ephemeral: boolean; + namespace: string; + name?: string; +} diff --git a/src/interfaces/constants.ts b/src/interfaces/constants.ts index 43406f04..93f0ed6c 100644 --- a/src/interfaces/constants.ts +++ b/src/interfaces/constants.ts @@ -124,4 +124,5 @@ export const FF_Paths = { // Status status: '/status', statusBatchManager: '/status/batchmanager', + statusWebsockets: '/status/websockets', }; diff --git a/src/interfaces/navigation.ts b/src/interfaces/navigation.ts index 38601101..3e99a42a 100644 --- a/src/interfaces/navigation.ts +++ b/src/interfaces/navigation.ts @@ -52,6 +52,7 @@ export const SUBSCRIPTIONS_PATH = 'subscriptions'; export const TOKENS_PATH = 'tokens'; export const TRANSACTIONS_PATH = 'transactions'; export const TRANSFERS_PATH = 'transfers'; +export const WEBSOCKETS_PATH = 'websockets'; export const FF_NAV_PATHS = { // Home @@ -162,6 +163,8 @@ export const FF_NAV_PATHS = { myNodePath: (ns: string) => `/${NAMESPACES_PATH}/${ns}/${MY_NODES_PATH}`, myNodeSubscriptionsPath: (ns: string) => `/${NAMESPACES_PATH}/${ns}/${MY_NODES_PATH}/${SUBSCRIPTIONS_PATH}`, + myNodeWebsocketsPath: (ns: string) => + `/${NAMESPACES_PATH}/${ns}/${MY_NODES_PATH}/${WEBSOCKETS_PATH}`, // Docs docsPath: DOCS_PATH, }; diff --git a/src/pages/Activity/views/Operations.tsx b/src/pages/Activity/views/Operations.tsx index bcb5450e..b1b78df1 100644 --- a/src/pages/Activity/views/Operations.tsx +++ b/src/pages/Activity/views/Operations.tsx @@ -186,7 +186,7 @@ export const ActivityOperations: () => JSX.Element = () => { value: , }, { - value: , + value: , }, { value: , diff --git a/src/pages/Home/views/Dashboard.tsx b/src/pages/Home/views/Dashboard.tsx index 853349d2..a5e8d567 100644 --- a/src/pages/Home/views/Dashboard.tsx +++ b/src/pages/Home/views/Dashboard.tsx @@ -1,4 +1,4 @@ -import { Grid, Typography } from '@mui/material'; +import { Grid } from '@mui/material'; import { BarDatum } from '@nivo/bar'; import dayjs from 'dayjs'; import React, { useContext, useEffect, useState } from 'react'; @@ -10,9 +10,10 @@ import { SkeletonCard } from '../../../components/Cards/EventCards/SkeletonCard' import { FireFlyCard } from '../../../components/Cards/FireFlyCard'; import { SmallCard } from '../../../components/Cards/SmallCard'; import { Histogram } from '../../../components/Charts/Histogram'; +import { MyNodeDiagram } from '../../../components/Charts/MyNodeDiagram'; import { Header } from '../../../components/Header'; +import { FFCircleLoader } from '../../../components/Loaders/FFCircleLoader'; import { NetworkMap } from '../../../components/NetworkMap/NetworkMap'; -import { HashPopover } from '../../../components/Popovers/HashPopover'; import { EventSlide } from '../../../components/Slides/EventSlide'; import { TransactionSlide } from '../../../components/Slides/TransactionSlide'; import { ApplicationContext } from '../../../contexts/ApplicationContext'; @@ -25,14 +26,15 @@ import { FF_EVENTS_CATEGORY_MAP, FF_NAV_PATHS, FF_OP_CATEGORY_MAP, - IDataWithHeader, IEvent, IFireFlyCard, IGenericPagedResponse, IMetric, - INode, ISmallCard, + IStatus, ITransaction, + IWebsocketConnection, + IWebsocketStatus, OpCategoryEnum, } from '../../../interfaces'; import { FF_Paths } from '../../../interfaces/constants'; @@ -51,16 +53,8 @@ import { hasAnyEvent } from '../../../utils/wsEvents'; export const HomeDashboard: () => JSX.Element = () => { const { t } = useTranslation(); - const { - newEvents, - lastRefreshTime, - clearNewEvents, - nodeID, - nodeName, - orgID, - orgName, - selectedNamespace, - } = useContext(ApplicationContext); + const { newEvents, lastRefreshTime, clearNewEvents, selectedNamespace } = + useContext(ApplicationContext); const { dateFilter } = useContext(DateFilterContext); const { slideID, setSlideSearchParam } = useContext(SlideContext); const { reportFetchError } = useContext(SnackbarContext); @@ -90,13 +84,23 @@ export const HomeDashboard: () => JSX.Element = () => { // Medium cards // Event types histogram const [eventHistData, setEventHistData] = useState(); - // My Node - const [myNode, setMyNode] = useState(); // Table cards const [recentEventTxs, setRecentEventTxs] = useState(); const [recentEvents, setRecentEvents] = useState(); const [isHistLoading, setIsHistLoading] = useState(false); + const [apps, setApps] = useState(); + const [plugins, setPlugins] = useState(); + + const [isMyNodeLoading, setIsMyNodeLoading] = useState(true); + + useEffect(() => { + setIsMounted(true); + return () => { + setIsMounted(false); + }; + }, []); + useEffect(() => { setIsMounted(true); return () => { @@ -259,33 +263,6 @@ export const HomeDashboard: () => JSX.Element = () => { }); }, [selectedNamespace, dateFilter, lastRefreshTime, isMounted]); - const myNodeDetailsList: IDataWithHeader[] = [ - { - header: t('nodeName'), - data: nodeName, - }, - { - header: t('nodeID'), - data: nodeID, - }, - { - header: t('orgName'), - data: orgName, - }, - { - header: t('orgID'), - data: orgID, - }, - { - header: t('profile'), - data: myNode?.profile?.id, - }, - { - header: t('profileEndpoint'), - data: myNode?.profile?.endpoint, - }, - ]; - const mediumCards: IFireFlyCard[] = [ { headerComponent: ( @@ -320,26 +297,17 @@ export const HomeDashboard: () => JSX.Element = () => { ), headerText: t('myNode'), - component: ( - - {myNodeDetailsList.map((data, idx) => ( - - - - {data.header} - - - - - ))} - - ), + component: + !isMyNodeLoading && + apps && + apps.length > 0 && + plugins && + Object.keys(plugins ?? {}).length > 0 && + isMounted ? ( + + ) : ( + + ), }, ]; @@ -365,15 +333,30 @@ export const HomeDashboard: () => JSX.Element = () => { reportFetchError(err); }) .finally(() => setIsHistLoading(false)); - fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.networkNodeById(nodeID)}`) - .then((nodeRes: INode) => { - setMyNode(nodeRes); + } + }, [selectedNamespace, dateFilter, lastRefreshTime, isMounted]); + + // useEffect for myNodeChart + useEffect(() => { + setIsMyNodeLoading(true); + if (isMounted) { + fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.statusWebsockets}`) + .then((wsRes: IWebsocketStatus) => { + isMounted && setApps(wsRes.connections); + }) + .catch((err) => { + reportFetchError(err); + }); + fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.status}`) + .then((statusRes: IStatus) => { + isMounted && setPlugins(statusRes.plugins); }) .catch((err) => { reportFetchError(err); }); + setIsMyNodeLoading(false); } - }, [selectedNamespace, dateFilter, lastRefreshTime, nodeID, isMounted]); + }, [selectedNamespace, isMounted]); const skeletonList = () => ( <> diff --git a/src/pages/MyNode/Routes.tsx b/src/pages/MyNode/Routes.tsx index 43947101..60c2e31e 100644 --- a/src/pages/MyNode/Routes.tsx +++ b/src/pages/MyNode/Routes.tsx @@ -2,6 +2,7 @@ import { RouteObject } from 'react-router-dom'; import { NAMESPACES_PATH } from '../../interfaces'; import { MyNodeDashboard } from './views/Dashboard'; import { MyNodeSubscriptions } from './views/Subscriptions'; +import { MyNodeWebsockets } from './views/Websockets'; export const MyNodeRoutes: RouteObject = { path: `${NAMESPACES_PATH}/:namespace/myNode`, @@ -15,5 +16,9 @@ export const MyNodeRoutes: RouteObject = { path: 'subscriptions', element: , }, + { + path: 'websockets', + element: , + }, ], }; diff --git a/src/pages/MyNode/views/Dashboard.tsx b/src/pages/MyNode/views/Dashboard.tsx index 64d92ecd..03f845f1 100644 --- a/src/pages/MyNode/views/Dashboard.tsx +++ b/src/pages/MyNode/views/Dashboard.tsx @@ -14,27 +14,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Grid, Typography } from '@mui/material'; +import { Grid } from '@mui/material'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { MyNodeDiagram } from '../../../components/Charts/MyNodeDiagram'; import { Header } from '../../../components/Header'; -import { FFTextField } from '../../../components/Inputs/FFTextField'; -import { FFCircleLoader } from '../../../components/Loaders/FFCircleLoader'; -import { ApplicationContext } from '../../../contexts/ApplicationContext'; import { SnackbarContext } from '../../../contexts/SnackbarContext'; -import { FF_Paths, INode, IOrganization } from '../../../interfaces'; -import { DEFAULT_PADDING, DEFAULT_SPACING } from '../../../theme'; +import { + FF_Paths, + IStatus, + IWebsocketConnection, + IWebsocketStatus, +} from '../../../interfaces'; +import { DEFAULT_PADDING } from '../../../theme'; import { fetchCatcher } from '../../../utils'; export const MyNodeDashboard: () => JSX.Element = () => { - const { nodeID, orgID } = useContext(ApplicationContext); const { reportFetchError } = useContext(SnackbarContext); const { t } = useTranslation(); + const [apps, setApps] = useState(); + const [plugins, setPlugins] = useState(); + const [isLoading, setIsLoading] = useState(true); + const [isMounted, setIsMounted] = useState(false); - // Node - const [node, setNode] = useState(); - // Org - const [org, setOrg] = useState(); useEffect(() => { setIsMounted(true); @@ -43,70 +45,26 @@ export const MyNodeDashboard: () => JSX.Element = () => { }; }, []); - // Nodes and Orgs useEffect(() => { + setIsLoading(true); if (isMounted) { - fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.networkNodeById(nodeID)}`) - .then((nodeRes: INode) => { - isMounted && setNode(nodeRes); + fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.statusWebsockets}`) + .then((wsRes: IWebsocketStatus) => { + isMounted && setApps(wsRes.connections); }) .catch((err) => { reportFetchError(err); }); - fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.networkOrgById(orgID)}`) - .then((orgRes: IOrganization) => { - isMounted && setOrg(orgRes); + fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.status}`) + .then((statusRes: IStatus) => { + isMounted && setPlugins(statusRes.plugins); }) .catch((err) => { reportFetchError(err); }); + setIsLoading(false); } - }, [nodeID, orgID, isMounted]); - - const nodeInputs = [ - { - defaultValue: node?.name ?? '', - label: t('name'), - }, - { - defaultValue: node?.did ?? '', - label: t('did'), - }, - { - defaultValue: node?.id ?? '', - label: t('id'), - }, - ]; - - const orgInputs = [ - { - defaultValue: org?.name ?? '', - label: t('name'), - }, - { - defaultValue: org?.did ?? '', - label: t('did'), - }, - { - defaultValue: org?.id ?? '', - label: t('id'), - }, - ]; - - const profileInputs = [ - { - defaultValue: node?.profile.id ?? '', - label: t('endpointID'), - }, - { - defaultValue: node?.profile.endpoint ?? '', - label: t('endpoint'), - }, - { - defaultValue: node?.profile.cert ?? '', - label: t('certificate'), - }, - ]; + }, [isMounted]); return ( <> @@ -117,89 +75,15 @@ export const MyNodeDashboard: () => JSX.Element = () => { noNsFilter > - - {!(node && org) ? ( - - ) : ( - <> - {/* Node */} - - {/* Node Title */} - - - {t('node')} - - - {/* Node Details */} - {nodeInputs.map((input, idx) => ( - - - - ))} - - {/* Org */} - - {/* Org Title */} - - - {t('organization')} - - - {/* Org Details */} - {orgInputs.map((input, idx) => ( - - - - ))} - - {/* Data Exchange */} - - {/* Profile Title */} - - - {t('profile')} - - - {/* Profile Details */} - {profileInputs.map((input, idx) => ( - - - - ))} - - - )} + + {!isLoading && + apps && + apps.length > 0 && + plugins && + Object.keys(plugins ?? {}).length > 0 && + isMounted && ( + + )} diff --git a/src/pages/MyNode/views/Websockets.tsx b/src/pages/MyNode/views/Websockets.tsx new file mode 100644 index 00000000..07d3a459 --- /dev/null +++ b/src/pages/MyNode/views/Websockets.tsx @@ -0,0 +1,168 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Grid } from '@mui/material'; +import React, { useContext, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Header } from '../../../components/Header'; +import { HashPopover } from '../../../components/Popovers/HashPopover'; +import { WebsocketSlide } from '../../../components/Slides/WebsocketSlide'; +import { FFTableText } from '../../../components/Tables/FFTableText'; +import { DataTable } from '../../../components/Tables/Table'; +import { SlideContext } from '../../../contexts/SlideContext'; +import { SnackbarContext } from '../../../contexts/SnackbarContext'; +import { + FF_Paths, + IDataTableRecord, + IWebsocketConnection, + IWebsocketStatus, +} from '../../../interfaces'; +import { DEFAULT_PADDING, DEFAULT_PAGE_LIMITS } from '../../../theme'; +import { fetchCatcher } from '../../../utils'; + +export const MyNodeWebsockets: () => JSX.Element = () => { + const { slideID, setSlideSearchParam } = useContext(SlideContext); + const { reportFetchError } = useContext(SnackbarContext); + const { t } = useTranslation(); + const [isMounted, setIsMounted] = useState(false); + + const [wsConns, setWsConns] = useState(); + const [wsConnsTotal, setWsConnsTotal] = useState(0); + const [viewWs, setViewWs] = useState(); + const [currentPage, setCurrentPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_PAGE_LIMITS[1]); + + useEffect(() => { + setIsMounted(true); + return () => { + setIsMounted(false); + }; + }, []); + + useEffect(() => { + isMounted && + slideID && + fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.statusWebsockets}`) + .then((wsRes: IWebsocketStatus) => { + if (isMounted) { + const filteredWS = wsRes.connections.filter( + (ws) => ws.id === slideID + ); + filteredWS.length > 0 && setViewWs(filteredWS[0]); + } + }) + .catch((err) => { + reportFetchError(err); + }); + }, [slideID, isMounted]); + + // Websockets + useEffect(() => { + isMounted && + fetchCatcher(`${FF_Paths.apiPrefix}/${FF_Paths.statusWebsockets}`) + .then((wsRes: IWebsocketStatus) => { + if (isMounted) { + setWsConns(wsRes.connections); + setWsConnsTotal(wsRes.connections.length); + } + }) + .catch((err) => { + reportFetchError(err); + }); + }, [isMounted]); + + const wsColHeaders = [ + t('id'), + t('remoteAddress'), + t('userAgent'), + t('numberOfSubscriptions'), + ]; + const wsRecords: IDataTableRecord[] | undefined = wsConns?.map((ws) => { + return { + key: ws.id, + columns: [ + { + value: , + }, + { + value: , + }, + { + value: ws.userAgent.length ? ( + + ) : ( + + ), + }, + { + value: ( + + ), + }, + ], + onClick: () => { + setViewWs(ws); + setSlideSearchParam(ws.id); + }, + }; + }); + + return ( + <> +
+ + + + setCurrentPage(currentPage) + } + onHandleRowsPerPage={(rowsPerPage: number) => + setRowsPerPage(rowsPerPage) + } + stickyHeader={true} + minHeight="300px" + maxHeight="calc(100vh - 340px)" + records={wsRecords} + columnHeaders={wsColHeaders} + paginate={true} + emptyStateText={t('noWebsocketConnectionsToDisplay')} + dataTotal={wsConnsTotal} + currentPage={currentPage} + rowsPerPage={rowsPerPage} + /> + + + {viewWs && ( + { + setViewWs(undefined); + setSlideSearchParam(null); + }} + /> + )} + + ); +}; diff --git a/src/pages/Off-Chain/views/Data.tsx b/src/pages/Off-Chain/views/Data.tsx index 3e9ec64d..8981b82a 100644 --- a/src/pages/Off-Chain/views/Data.tsx +++ b/src/pages/Off-Chain/views/Data.tsx @@ -119,6 +119,7 @@ export const OffChainData: () => JSX.Element = () => { t('blobName'), t('blobSize'), t('created'), + t(''), ]; const dataRecords: IDataTableRecord[] | undefined = data?.map((d) => ({ diff --git a/src/pages/Tokens/views/PoolDetails.tsx b/src/pages/Tokens/views/PoolDetails.tsx index 73d13514..9a0544d8 100644 --- a/src/pages/Tokens/views/PoolDetails.tsx +++ b/src/pages/Tokens/views/PoolDetails.tsx @@ -30,6 +30,7 @@ import { TransferSlide } from '../../../components/Slides/TransferSlide'; import { FFTableText } from '../../../components/Tables/FFTableText'; import { MediumCardTable } from '../../../components/Tables/MediumCardTable'; import { DataTable } from '../../../components/Tables/Table'; +import { FFJsonViewer } from '../../../components/Viewers/FFJsonViewer'; import { ApplicationContext } from '../../../contexts/ApplicationContext'; import { DateFilterContext } from '../../../contexts/DateFilterContext'; import { SlideContext } from '../../../contexts/SlideContext'; @@ -177,22 +178,17 @@ export const PoolDetails: () => JSX.Element = () => { }, ]; - const poolAccountsColHeaders = [t('key'), t('balance'), t('lastUpdated')]; + const poolAccountsColHeaders = [t('key'), t('balance')]; const poolAccountsRecords: IDataTableRecord[] | undefined = poolAccounts?.map( (account, idx) => ({ key: idx.toString(), columns: [ { - value: , + value: , }, { value: , }, - { - value: ( - - ), - }, ], }) ); @@ -214,6 +210,11 @@ export const PoolDetails: () => JSX.Element = () => { ), }; + const infoCard = { + headerText: t('poolInfo'), + component: pool?.info && , + }; + const tokenTransferColHeaders = [ t('activity'), t('from'), @@ -295,7 +296,7 @@ export const PoolDetails: () => JSX.Element = () => { direction="row" justifyContent="flex-center" alignItems="flex-start" - xs={6} + xs={5} pr={DEFAULT_PADDING} > {/* Pool Card */} @@ -337,6 +338,26 @@ export const PoolDetails: () => JSX.Element = () => {
{/* Right hand side */} + + {/* Transfers */} + + + + JSX.Element = () => { direction="row" justifyContent="flex-center" alignItems="flex-start" - xs={6} + xs={3} > - {/* Transfers */} + {/* Accounts */}