diff --git a/src/App.tsx b/src/App.tsx index bd3a7691..37e6df06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,6 @@ import { createTheme, CssBaseline, StyledEngineProvider, - Theme, ThemeProvider, } from '@mui/material'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -31,17 +30,22 @@ import { } from './components/Snackbar/MessageSnackbar'; import { ApplicationContext } from './contexts/ApplicationContext'; import { SnackbarContext } from './contexts/SnackbarContext'; -import { FF_EVENTS, INamespace, IStatus, NAMESPACES_PATH } from './interfaces'; +import { + FF_EVENTS, + INamespace, + INewEventSet, + IStatus, + NAMESPACES_PATH, +} from './interfaces'; import { FF_Paths } from './interfaces/constants'; import { themeOptions } from './theme'; import { fetchWithCredentials, summarizeFetchError } from './utils'; -//TODO: remove along with useStyles() usage -declare module '@mui/styles/defaultTheme' { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface DefaultTheme extends Theme {} -} -export const theme = createTheme(themeOptions); +const makeNewEventMap = (): INewEventSet => { + const map: any = {}; + Object.values(FF_EVENTS).map((v) => (map[v] = false)); + return map; +}; const App: React.FC = () => { const [initialized, setInitialized] = useState(false); @@ -58,7 +62,7 @@ const App: React.FC = () => { const [nodeName, setNodeName] = useState(''); const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; // Event Context - const [newEvents, setNewEvents] = useState([]); + const [newEvents, setNewEvents] = useState(makeNewEventMap()); const [lastRefreshTime, setLastRefresh] = useState( new Date().toISOString() ); @@ -123,8 +127,13 @@ const App: React.FC = () => { ws.current.onmessage = (event: any) => { const eventData = JSON.parse(event.data); const eventType: FF_EVENTS = eventData.type; - if (Object.values(FF_EVENTS).includes(eventType)) { - setNewEvents((existing) => [eventType, ...existing]); + if ( + !newEvents[eventType] && + Object.values(FF_EVENTS).includes(eventType) + ) { + setNewEvents((existing) => { + return { ...existing, [eventType]: true }; + }); } }; @@ -144,17 +153,19 @@ const App: React.FC = () => { }; const clearNewEvents = () => { - setNewEvents([]); + setNewEvents(makeNewEventMap()); setLastRefresh(new Date().toISOString()); }; + const theme = createTheme(themeOptions); + if (initialized) { if (initError) { // figure out what to display return ( <> - + Fallback @@ -181,7 +192,7 @@ const App: React.FC = () => { value={{ setMessage, setMessageType, reportFetchError }} > - + = ({ api, isOpen = false }) => { + const { selectedNamespace } = useContext(ApplicationContext); const { t } = useTranslation(); const [expanded, setExpanded] = useState(isOpen); @@ -42,9 +50,20 @@ export const ApiAccordion: React.FC = ({ api, isOpen = false }) => { }> + + } + rightContent={ + } - rightContent={} /> @@ -62,13 +81,9 @@ export const ApiAccordion: React.FC = ({ api, isOpen = false }) => { ))} {/* OpenAPI */} - {api.urls.openapi && ( - - )} - {/* Swagger */} - {api.urls.ui && ( - - )} + + {/* UI */} + ); diff --git a/src/components/Accordions/BlockchainEventAccordion.tsx b/src/components/Accordions/BlockchainEventAccordion.tsx index ffaecab3..dfae010d 100644 --- a/src/components/Accordions/BlockchainEventAccordion.tsx +++ b/src/components/Accordions/BlockchainEventAccordion.tsx @@ -5,10 +5,17 @@ import { AccordionSummary, Grid, } from '@mui/material'; -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { IBlockchainEvent, IDataWithHeader } from '../../interfaces'; +import { ApplicationContext } from '../../contexts/ApplicationContext'; +import { + FF_NAV_PATHS, + IBlockchainEvent, + IDataWithHeader, +} from '../../interfaces'; +import { DEFAULT_PADDING } from '../../theme'; import { getFFTime } from '../../utils'; +import { LaunchButton } from '../Buttons/LaunchButton'; import { HashPopover } from '../Popovers/HashPopover'; import { FFJsonViewer } from '../Viewers/FFJsonViewer'; import { FFAccordionHeader } from './FFAccordionHeader'; @@ -23,6 +30,7 @@ export const BlockchainEventAccordion: React.FC = ({ be, isOpen = false, }) => { + const { selectedNamespace } = useContext(ApplicationContext); const { t } = useTranslation(); const [expanded, setExpanded] = useState(isOpen); @@ -59,37 +67,49 @@ export const BlockchainEventAccordion: React.FC = ({ /> } rightContent={ - + } /> - - {accInfo.map((info, idx) => ( - - - {info.data} - - ))} - {be.info && ( - + )} {be.output && ( - + )} + + {accInfo.map((info, idx) => ( + + + {info.data} + + ))} + ); diff --git a/src/components/Accordions/FFAccordionText.tsx b/src/components/Accordions/FFAccordionText.tsx index 3067e36c..73c4d074 100644 --- a/src/components/Accordions/FFAccordionText.tsx +++ b/src/components/Accordions/FFAccordionText.tsx @@ -16,7 +16,7 @@ export const FFAccordionText: React.FC = ({ return ( = ({ listener, isOpen = false, }) => { + const { selectedNamespace } = useContext(ApplicationContext); const { t } = useTranslation(); const [expanded, setExpanded] = useState(isOpen); @@ -31,13 +38,11 @@ export const ListenerAccordion: React.FC = ({ data: , }, { - header: t('protocolID'), - data: , - }, - { - header: t('location'), - data: ( - + header: t('topic'), + data: listener.topic ? ( + + ) : ( + ), }, { @@ -67,10 +72,11 @@ export const ListenerAccordion: React.FC = ({ /> } rightContent={ - } /> @@ -79,7 +85,7 @@ export const ListenerAccordion: React.FC = ({ {/* Basic Data */} {accInfo.map((info, idx) => ( - + = ({ - param, - isOpen = false, -}) => { - const [expanded, setExpanded] = useState(isOpen); - - return ( - setExpanded(!expanded)} - > - }> - - } - /> - - - {/* Basic Data */} - - - - - - - - ); -}; diff --git a/src/components/Accordions/MessageAccordion.tsx b/src/components/Accordions/MessageAccordion.tsx index c916856f..82cb565a 100644 --- a/src/components/Accordions/MessageAccordion.tsx +++ b/src/components/Accordions/MessageAccordion.tsx @@ -3,18 +3,21 @@ import { Accordion, AccordionDetails, AccordionSummary, - Chip, Grid, } from '@mui/material'; -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ApplicationContext } from '../../contexts/ApplicationContext'; import { FF_MESSAGES_CATEGORY_MAP, + FF_NAV_PATHS, IDataWithHeader, IMessage, - MsgStateColorMap, } from '../../interfaces'; +import { DEFAULT_PADDING } from '../../theme'; import { getFFTime } from '../../utils'; +import { LaunchButton } from '../Buttons/LaunchButton'; +import { MsgStatusChip } from '../Chips/MsgStatusChip'; import { HashPopover } from '../Popovers/HashPopover'; import { FFAccordionHeader } from './FFAccordionHeader'; import { FFAccordionText } from './FFAccordionText'; @@ -28,6 +31,7 @@ export const MessageAccordion: React.FC = ({ message, isOpen = false, }) => { + const { selectedNamespace } = useContext(ApplicationContext); const { t } = useTranslation(); const [expanded, setExpanded] = useState(isOpen); @@ -70,29 +74,20 @@ export const MessageAccordion: React.FC = ({ /> } rightContent={ - message.state && ( - // TODO: Fix when https://github.com/hyperledger/firefly/issues/628 is resolved - - ) + <> + {message.state && } + + } /> - + {accInfo.map((info, idx) => ( = ({ data, isOpen = false, + showLink = true, }) => { const { selectedNamespace } = useContext(ApplicationContext); const { t } = useTranslation(); @@ -65,30 +68,47 @@ export const MessageDataAccordion: React.FC = ({ leftContent={} rightContent={ <> - { - e.stopPropagation(); - setOpenDataModal(true); - }} - > - - - { - e.stopPropagation(); - navigate( - FF_NAV_PATHS.offchainDataPath(selectedNamespace, data.id) - ); - }} - > - - + {data.blob && ( + + )} + {showLink && ( + { + e.stopPropagation(); + navigate( + FF_NAV_PATHS.offchainDataPath( + selectedNamespace, + data.id + ) + ); + }} + > + + + )} } /> - + {data.value && ( + + + + + + + )} + {accInfo.map((info, idx) => ( = ({ op, isOpen = false }) => { isHeader /> } - rightContent={ - // TODO: Fix when https://github.com/hyperledger/firefly/issues/628 is resolved - - } + rightContent={} /> diff --git a/src/components/Accordions/TransactionAccordion.tsx b/src/components/Accordions/TransactionAccordion.tsx index 171a2061..088cadc2 100644 --- a/src/components/Accordions/TransactionAccordion.tsx +++ b/src/components/Accordions/TransactionAccordion.tsx @@ -5,11 +5,13 @@ import { AccordionSummary, Grid, } from '@mui/material'; -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { IDataWithHeader, ITransaction } from '../../interfaces'; +import { ApplicationContext } from '../../contexts/ApplicationContext'; +import { FF_NAV_PATHS, IDataWithHeader, ITransaction } from '../../interfaces'; import { FF_TX_CATEGORY_MAP } from '../../interfaces/enums/transactionTypes'; import { getFFTime } from '../../utils'; +import { LaunchButton } from '../Buttons/LaunchButton'; import { HashPopover } from '../Popovers/HashPopover'; import { FFAccordionHeader } from './FFAccordionHeader'; import { FFAccordionText } from './FFAccordionText'; @@ -23,6 +25,7 @@ export const TransactionAccordion: React.FC = ({ tx, isOpen = false, }) => { + const { selectedNamespace } = useContext(ApplicationContext); const { t } = useTranslation(); const [expanded, setExpanded] = useState(isOpen); @@ -65,7 +68,17 @@ export const TransactionAccordion: React.FC = ({ isHeader /> } - rightContent={} + rightContent={ + <> + + + + } /> diff --git a/src/components/AppWrapper.tsx b/src/components/AppWrapper.tsx index 8222b556..695612bf 100644 --- a/src/components/AppWrapper.tsx +++ b/src/components/AppWrapper.tsx @@ -15,7 +15,7 @@ import { NAMESPACES_PATH, TimeFilterEnum, } from '../interfaces'; -import { getTimeFilterObject, isValidUUID } from '../utils'; +import { getTimeFilterObject } from '../utils'; import { Navigation, NAV_WIDTH } from './Navigation/Navigation'; const Main = styled('main')({ @@ -39,7 +39,7 @@ export const TIME_QUERY_KEY = 'time'; export const AppWrapper: React.FC = () => { const { pathname, search } = useLocation(); - const { selectedNamespace } = useContext(ApplicationContext); + const { selectedNamespace, clearNewEvents } = useContext(ApplicationContext); const [filterAnchor, setFilterAnchor] = useState( null ); @@ -66,6 +66,10 @@ export const AppWrapper: React.FC = () => { initializeTableFilterSearchParams(); }, [pathname, search]); + useEffect(() => { + clearNewEvents(); + }, [pathname]); + const initializeTimeSearchParams = () => { // If date has already been set if ( @@ -99,7 +103,7 @@ export const AppWrapper: React.FC = () => { const initializeSlideSearchParams = () => { setSlideID(null); const existingSlideParam = searchParams.get(SLIDE_QUERY_KEY); - if (existingSlideParam === null || !isValidUUID(existingSlideParam)) { + if (existingSlideParam === null) { setSlideSearchParam(null); } else { setSlideSearchParam(existingSlideParam); @@ -110,7 +114,7 @@ export const AppWrapper: React.FC = () => { if (slideID === null) { searchParams.delete(SLIDE_QUERY_KEY); setSearchParams(searchParams); - } else if (isValidUUID(slideID)) { + } else { searchParams.set(SLIDE_QUERY_KEY, slideID); setSearchParams(searchParams); setSlideID(slideID); diff --git a/src/components/Buttons/DownloadButton.tsx b/src/components/Buttons/DownloadButton.tsx new file mode 100644 index 00000000..d60547d1 --- /dev/null +++ b/src/components/Buttons/DownloadButton.tsx @@ -0,0 +1,24 @@ +import { Download } from '@mui/icons-material'; +import { IconButton } from '@mui/material'; +import { downloadBlobFile, downloadExternalFile } from '../../utils'; + +interface Props { + filename?: string; + isBlob: boolean; + url: string; +} + +export const DownloadButton: React.FC = ({ filename, isBlob, url }) => { + return ( + { + e.stopPropagation(); + isBlob + ? downloadBlobFile(url, filename) + : downloadExternalFile(url, filename); + }} + > + + + ); +}; diff --git a/src/components/Buttons/InterfaceButton.tsx b/src/components/Buttons/InterfaceButton.tsx new file mode 100644 index 00000000..290e6ac5 --- /dev/null +++ b/src/components/Buttons/InterfaceButton.tsx @@ -0,0 +1,31 @@ +import LaunchIcon from '@mui/icons-material/Launch'; +import { IconButton } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { FF_NAV_PATHS } from '../../interfaces'; +import { FFColors } from '../../theme'; + +interface Props { + ns: string; + interfaceID: string; + small?: boolean; +} + +export const InterfaceButton: React.FC = ({ + ns, + interfaceID, + small = false, +}) => { + const navigate = useNavigate(); + return ( + { + e.stopPropagation(); + navigate(FF_NAV_PATHS.blockchainInterfacesPath(ns, interfaceID)); + }} + > + + + ); +}; diff --git a/src/components/Buttons/LaunchButton.tsx b/src/components/Buttons/LaunchButton.tsx new file mode 100644 index 00000000..95522077 --- /dev/null +++ b/src/components/Buttons/LaunchButton.tsx @@ -0,0 +1,16 @@ +import { Launch } from '@mui/icons-material'; +import { IconButton, Link } from '@mui/material'; + +interface Props { + link: string; +} + +export const LaunchButton: React.FC = ({ link }) => { + return ( + + + + + + ); +}; diff --git a/src/components/Buttons/OpRetryButton.tsx b/src/components/Buttons/OpRetryButton.tsx new file mode 100644 index 00000000..6395a9a4 --- /dev/null +++ b/src/components/Buttons/OpRetryButton.tsx @@ -0,0 +1,22 @@ +import LaunchIcon from '@mui/icons-material/Launch'; +import { IconButton } from '@mui/material'; +import { useContext } from 'react'; +import { SlideContext } from '../../contexts/SlideContext'; +import { FFColors } from '../../theme'; + +interface Props { + retryOpID: string; +} + +export const OpRetryButton: React.FC = ({ retryOpID }) => { + const { setSlideSearchParam } = useContext(SlideContext); + + return ( + setSlideSearchParam(retryOpID)} + > + + + ); +}; diff --git a/src/components/Cards/EmptyStateCard.tsx b/src/components/Cards/EmptyStateCard.tsx index 615dec7e..c7819a66 100644 --- a/src/components/Cards/EmptyStateCard.tsx +++ b/src/components/Cards/EmptyStateCard.tsx @@ -2,7 +2,7 @@ import { Grid, Typography } from '@mui/material'; import { DEFAULT_PADDING } from '../../theme'; type Props = { - height?: number; + height?: string | number; text: string; subText?: string; }; diff --git a/src/components/Cards/SmallCard.tsx b/src/components/Cards/SmallCard.tsx index 8cd79014..358c9cfe 100644 --- a/src/components/Cards/SmallCard.tsx +++ b/src/components/Cards/SmallCard.tsx @@ -87,7 +87,7 @@ export const SmallCard: React.FC = ({ card }) => { {data.data !== undefined ? ( {data.data} diff --git a/src/components/Charts/Histogram.tsx b/src/components/Charts/Histogram.tsx index d0679a04..9f8f8e93 100644 --- a/src/components/Charts/Histogram.tsx +++ b/src/components/Charts/Histogram.tsx @@ -1,4 +1,4 @@ -import { Paper, Typography } from '@mui/material'; +import { Grid, Paper, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { Box } from '@mui/system'; import { BarDatum, ResponsiveBar } from '@nivo/bar'; @@ -54,104 +54,109 @@ export const Histogram: React.FC = ({ {!data || isLoading ? ( ) : isEmpty ? ( - + ) : ( - - xAxisValues?.find((vts) => vts === v) - ? dayjs(v).format('h:mm') - : '', - }} - axisLeft={{ - tickSize: 5, - tickPadding: 5, - tickRotation: 0, - tickValues: 5, - }} - legends={ - includeLegend - ? [ - { - dataFrom: 'keys', - anchor: 'bottom', - direction: 'row', - justify: false, - translateX: 20, - translateY: 50, - itemsSpacing: 2, - itemWidth: 100, - itemHeight: 10, - itemDirection: 'left-to-right', - itemOpacity: 1, - itemTextColor: theme.palette.text.primary, - symbolSize: 15, - symbolShape: 'circle', + + + xAxisValues?.find((vts) => vts === v) + ? dayjs(v).format('h:mm') + : '', + }} + axisLeft={{ + tickSize: 5, + tickPadding: 5, + tickRotation: 0, + tickValues: 5, + }} + legends={ + includeLegend + ? [ + { + dataFrom: 'keys', + anchor: 'bottom', + direction: 'row', + justify: false, + translateX: 20, + translateY: 50, + itemsSpacing: 2, + itemWidth: 100, + itemHeight: 10, + itemDirection: 'left-to-right', + itemOpacity: 1, + itemTextColor: theme.palette.text.primary, + symbolSize: 15, + symbolShape: 'circle', + }, + ] + : undefined + } + motionConfig="stiff" + enableLabel={false} + role="application" + theme={{ + background: theme.palette.background.paper, + axis: { + ticks: { + line: { + stroke: theme.palette.background.default, }, - ] - : undefined - } - motionConfig="stiff" - enableLabel={false} - role="application" - theme={{ - background: theme.palette.background.paper, - axis: { - ticks: { + text: { + fill: theme.palette.text.disabled, + }, + }, + }, + grid: { line: { stroke: theme.palette.background.default, }, - text: { - fill: theme.palette.text.disabled, - }, - }, - }, - grid: { - line: { - stroke: theme.palette.background.default, }, - }, - }} - tooltip={({ data }) => { - return ( - - {keys.map((key, idx) => { - return ( - - {`${key.toUpperCase()}: ${data[key] ?? 0}`} - - ); - })} - - {getFFTime(data.timestamp.toString())} - - - {getFFTime(data.timestamp.toString(), true)} - - - ); - }} - /> + }} + tooltip={({ data }) => { + return ( + + {keys.map((key, idx) => { + return ( + + {`${key.toUpperCase()}: ${data[key] ?? 0}`} + + ); + })} + + {getFFTime(data.timestamp.toString())} + + + {getFFTime(data.timestamp.toString(), true)} + + + ); + }} + /> + )} ); diff --git a/src/components/Chips/MsgStatusChip.tsx b/src/components/Chips/MsgStatusChip.tsx new file mode 100644 index 00000000..dcd7eb20 --- /dev/null +++ b/src/components/Chips/MsgStatusChip.tsx @@ -0,0 +1,34 @@ +// 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 { Chip } from '@mui/material'; +import React from 'react'; +import { IMessage, MsgStateColorMap } from '../../interfaces'; + +interface Props { + msg: IMessage; +} + +export const MsgStatusChip: React.FC = ({ msg }) => { + return ( + + ); +}; diff --git a/src/components/Chips/OpStatusChip.tsx b/src/components/Chips/OpStatusChip.tsx new file mode 100644 index 00000000..69f06a22 --- /dev/null +++ b/src/components/Chips/OpStatusChip.tsx @@ -0,0 +1,44 @@ +// 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 { Chip } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { IOperation, OpStatusColorMap } from '../../interfaces'; + +interface Props { + op: IOperation; +} + +export const OpStatusChip: React.FC = ({ op }) => { + const { t } = useTranslation(); + + return op.retry ? ( + + ) : ( + + ); +}; diff --git a/src/components/Chips/PoolStatusChip.tsx b/src/components/Chips/PoolStatusChip.tsx new file mode 100644 index 00000000..05231ae9 --- /dev/null +++ b/src/components/Chips/PoolStatusChip.tsx @@ -0,0 +1,32 @@ +// 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 { Chip } from '@mui/material'; +import React from 'react'; +import { ITokenPool, PoolStateColorMap } from '../../interfaces'; + +interface Props { + pool: ITokenPool; +} + +export const PoolStatusChip: React.FC = ({ pool }) => { + return ( + + ); +}; diff --git a/src/components/Chips/TxStatusChip.tsx b/src/components/Chips/TxStatusChip.tsx new file mode 100644 index 00000000..20e6935e --- /dev/null +++ b/src/components/Chips/TxStatusChip.tsx @@ -0,0 +1,34 @@ +// 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 { Chip } from '@mui/material'; +import React from 'react'; +import { ITxStatus, TxStatusColorMap } from '../../interfaces'; + +interface Props { + txStatus: ITxStatus; +} + +export const TxStatusChip: React.FC = ({ txStatus }) => { + return ( + + ); +}; diff --git a/src/components/Filters/FilterButton.tsx b/src/components/Filters/FilterButton.tsx index f6c29018..0d278fd1 100644 --- a/src/components/Filters/FilterButton.tsx +++ b/src/components/Filters/FilterButton.tsx @@ -51,11 +51,11 @@ export const FilterButton: React.FC = ({ onSetFilterAnchor }) => { > {filterArray.map((filter, index) => ( - + removeFilter(filter)} label={filter} /> ))} - + = ({ onSetFilterAnchor }) => { )} - + - + ) => + setFilterAnchor(e.currentTarget) + } + /> } /> JSX.Element = () => { /> + {filterAnchor && ( + { + setFilterAnchor(null); + }} + fields={InterfaceFilters} + /> + )} {viewInterface && ( JSX.Element = () => { t('eventName'), t('id'), t('interfaceID'), - t('protocolID'), - t('location'), + t('fireflyID'), t('created'), ]; @@ -144,14 +143,6 @@ export const BlockchainListeners: () => JSX.Element = () => { ), }, - { - value: ( - - ), - }, { value: , }, @@ -169,7 +160,6 @@ export const BlockchainListeners: () => JSX.Element = () => { ) => diff --git a/src/pages/Home/views/Dashboard.tsx b/src/pages/Home/views/Dashboard.tsx index 5d5d7757..86ffe18c 100644 --- a/src/pages/Home/views/Dashboard.tsx +++ b/src/pages/Home/views/Dashboard.tsx @@ -48,6 +48,7 @@ import { makeColorArray, makeKeyArray, } from '../../../utils/charts'; +import { hasAnyEvent } from '../../../utils/wsEvents'; export const HomeDashboard: () => JSX.Element = () => { const { t } = useTranslation(); @@ -73,8 +74,9 @@ export const HomeDashboard: () => JSX.Element = () => { const [blockchainTxCount, setBlockchainTxCount] = useState(); const [blockchainEventCount, setBlockchainEventCount] = useState(); // Messages - const [messagesTxCount, setMessagesTxCount] = useState(); - const [messagesEventCount, setMessagesEventCount] = useState(); + const [messagesBroadcastCount, setMessagesBroadcastCount] = + useState(); + const [messagesPrivateCount, setMessagesPrivateCount] = useState(); // Tokens const [tokenTransfersCount, setTokenTransfersCount] = useState(); const [tokenMintCount, setTokenMintcount] = useState(); @@ -123,7 +125,7 @@ export const HomeDashboard: () => JSX.Element = () => { { header: t('blockchain'), data: [ - { header: t('tx'), data: blockchainTxCount }, + { header: t('transactions'), data: blockchainTxCount }, { header: t('events'), data: blockchainEventCount }, ], clickPath: FF_NAV_PATHS.blockchainPath(selectedNamespace), @@ -131,8 +133,8 @@ export const HomeDashboard: () => JSX.Element = () => { { header: t('messages'), data: [ - { header: t('tx'), data: messagesTxCount }, - { header: t('events'), data: messagesEventCount }, + { header: t('broadcast'), data: messagesBroadcastCount }, + { header: t('private'), data: messagesPrivateCount }, ], clickPath: FF_NAV_PATHS.offchainPath(selectedNamespace), }, @@ -225,8 +227,8 @@ export const HomeDashboard: () => JSX.Element = () => { ([ blockchainTx, blockchainEvents, - msgsTx, - msgsEvents, + msgsBroadcast, + msgPrivate, tokensTransfer, tokensMint, tokensBurn, @@ -240,8 +242,8 @@ export const HomeDashboard: () => JSX.Element = () => { setBlockchainTxCount(blockchainTx.total); setBlockchainEventCount(blockchainEvents.total); // Messages - setMessagesEventCount(msgsTx.total); - setMessagesTxCount(msgsEvents.total); + setMessagesBroadcastCount(msgsBroadcast.total); + setMessagesPrivateCount(msgPrivate.total); // Tokens setTokenTransfersCount(tokensTransfer.total); setTokenMintcount(tokensMint.total); @@ -336,8 +338,14 @@ export const HomeDashboard: () => JSX.Element = () => { {myNodeDetailsList.map((data, idx) => ( - - + + {data.header} @@ -499,9 +507,10 @@ export const HomeDashboard: () => JSX.Element = () => { setViewTx(tx); setSlideSearchParam(tx.id); }} - link={FF_NAV_PATHS.activityTxDetailPath( + link={FF_NAV_PATHS.activityTxDetailPathWithSlide( selectedNamespace, - event.tx + event.tx, + event.id )} {...{ event }} /> @@ -543,7 +552,7 @@ export const HomeDashboard: () => JSX.Element = () => {
0} + showRefreshBtn={hasAnyEvent(newEvents)} onRefresh={clearNewEvents} >
@@ -560,7 +569,9 @@ export const HomeDashboard: () => JSX.Element = () => { return ( JSX.Element = () => { justifyContent="center" container item - xs={4} + md={12} + lg={4} > @@ -614,7 +626,8 @@ export const HomeDashboard: () => JSX.Element = () => { justifyContent="center" container item - xs={6} + sm={12} + md={6} > diff --git a/src/pages/MyNode/views/Subscriptions.tsx b/src/pages/MyNode/views/Subscriptions.tsx index fde5054c..280f620a 100644 --- a/src/pages/MyNode/views/Subscriptions.tsx +++ b/src/pages/MyNode/views/Subscriptions.tsx @@ -141,7 +141,6 @@ export const MyNodeSubscriptions: () => JSX.Element = () => { ) => diff --git a/src/pages/Network/views/Identities.tsx b/src/pages/Network/views/Identities.tsx index d5bc5905..6681b90e 100644 --- a/src/pages/Network/views/Identities.tsx +++ b/src/pages/Network/views/Identities.tsx @@ -14,7 +14,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import PermIdentityIcon from '@mui/icons-material/PermIdentity'; import { Grid } from '@mui/material'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -105,13 +104,7 @@ export const NetworkIdentities: () => JSX.Element = () => { key: id.id, columns: [ { - value: ( - } - /> - ), + value: , }, { value: , @@ -150,7 +143,6 @@ export const NetworkIdentities: () => JSX.Element = () => { ) => diff --git a/src/pages/Network/views/Nodes.tsx b/src/pages/Network/views/Nodes.tsx index cc275eaf..a99055c7 100644 --- a/src/pages/Network/views/Nodes.tsx +++ b/src/pages/Network/views/Nodes.tsx @@ -14,7 +14,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import HexagonIcon from '@mui/icons-material/Hexagon'; import { Chip, Grid } from '@mui/material'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -94,13 +93,7 @@ export const NetworkNodes: () => JSX.Element = () => { key: node.id, columns: [ { - value: ( - } - /> - ), + value: , }, { value: , @@ -142,7 +135,6 @@ export const NetworkNodes: () => JSX.Element = () => { ) => diff --git a/src/pages/Network/views/Organizations.tsx b/src/pages/Network/views/Organizations.tsx index dbe65bd8..a90a51da 100644 --- a/src/pages/Network/views/Organizations.tsx +++ b/src/pages/Network/views/Organizations.tsx @@ -14,7 +14,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import HiveIcon from '@mui/icons-material/Hive'; import { Chip, Grid } from '@mui/material'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -96,11 +95,7 @@ export const NetworkOrganizations: () => JSX.Element = () => { { value: ( <> - } - /> + ), }, @@ -144,7 +139,6 @@ export const NetworkOrganizations: () => JSX.Element = () => { ) => diff --git a/src/pages/Off-Chain/views/Dashboard.tsx b/src/pages/Off-Chain/views/Dashboard.tsx index 3fd39f6a..7f6a078f 100644 --- a/src/pages/Off-Chain/views/Dashboard.tsx +++ b/src/pages/Off-Chain/views/Dashboard.tsx @@ -15,18 +15,21 @@ // limitations under the License. import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import DownloadIcon from '@mui/icons-material/Download'; -import { Chip, Grid, IconButton } from '@mui/material'; +import { Grid, IconButton } from '@mui/material'; import { BarDatum } from '@nivo/bar'; import dayjs from 'dayjs'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import { DownloadButton } from '../../../components/Buttons/DownloadButton'; import { FireFlyCard } from '../../../components/Cards/FireFlyCard'; import { SmallCard } from '../../../components/Cards/SmallCard'; import { Histogram } from '../../../components/Charts/Histogram'; +import { MsgStatusChip } from '../../../components/Chips/MsgStatusChip'; import { Header } from '../../../components/Header'; import { HashPopover } from '../../../components/Popovers/HashPopover'; +import { DataSlide } from '../../../components/Slides/DataSlide'; +import { DatatypeSlide } from '../../../components/Slides/DatatypeSlide'; import { MessageSlide } from '../../../components/Slides/MessageSlide'; import { FFTableText } from '../../../components/Tables/FFTableText'; import { MediumCardTable } from '../../../components/Tables/MediumCardTable'; @@ -41,7 +44,6 @@ import { DATATYPES_PATH, DATA_PATH, FF_MESSAGES_CATEGORY_MAP, - FF_NAV_PATHS, FF_Paths, IData, IDataTableRecord, @@ -53,7 +55,6 @@ import { IPagedMessageResponse, ISmallCard, MESSAGES_PATH, - MsgStateColorMap, } from '../../../interfaces'; import { FF_TX_CATEGORY_MAP } from '../../../interfaces/enums/transactionTypes'; import { @@ -61,12 +62,7 @@ import { DEFAULT_PAGE_LIMITS, DEFAULT_SPACING, } from '../../../theme'; -import { - downloadBlobFile, - fetchCatcher, - getFFTime, - makeMsgHistogram, -} from '../../../utils'; +import { fetchCatcher, getFFTime, makeMsgHistogram } from '../../../utils'; import { isHistogramEmpty, makeColorArray, @@ -90,6 +86,8 @@ export const OffChainDashboard: () => JSX.Element = () => { const [dataCount, setDataCount] = useState(); // Datatypes const [datatypesCount, setDatatypesCount] = useState(); + // Blobs + const [blobCount, setBlobCount] = useState(); // Medium cards // Messages histogram @@ -105,6 +103,8 @@ export const OffChainDashboard: () => JSX.Element = () => { const [messageTotal, setMessageTotal] = useState(0); // View message slide out const [viewMsg, setViewMsg] = useState(); + const [viewData, setViewData] = useState(); + const [viewDatatype, setViewDatatype] = useState(); const [currentPage, setCurrentPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_PAGE_LIMITS[0]); @@ -118,19 +118,25 @@ export const OffChainDashboard: () => JSX.Element = () => { }, []); useEffect(() => { - isMounted && - slideID && + if (isMounted && slideID) { fetchCatcher( `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.messagesById( slideID )}` - ) - .then((messageRes: IMessage) => { - setViewMsg(messageRes); - }) - .catch((err) => { - reportFetchError(err); - }); + ).then((messageRes: IMessage) => { + setViewMsg(messageRes); + }); + fetchCatcher( + `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.dataById(slideID)}` + ).then((dataRes: IData) => { + setViewData(dataRes); + }); + fetchCatcher( + `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.datatypes}?id=${slideID}` + ).then((dtRes: IDatatype[]) => { + dtRes.length === 1 && setViewDatatype(dtRes[0]); + }); + } }, [slideID, isMounted]); const smallCards: ISmallCard[] = [ @@ -144,15 +150,16 @@ export const OffChainDashboard: () => JSX.Element = () => { data: [{ data: dataCount }], clickPath: `${DATA_PATH}`, }, + { + header: t('blobs'), + data: [{ data: blobCount }], + clickPath: `${DATA_PATH}?filters=blob.hash=!=`, + }, { header: t('totalDatatypes'), data: [{ data: datatypesCount }], clickPath: DATATYPES_PATH, }, - { - header: t('totalFileSize'), - data: [{ data: 0 }], - }, ]; // Small Card UseEffect @@ -171,6 +178,10 @@ export const OffChainDashboard: () => JSX.Element = () => { fetchCatcher( `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.data}${qParams}` ), + // Blobs + fetchCatcher( + `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.data}${qParams}&blob.hash=!?` + ), // Datatypes fetchCatcher( `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.datatypes}${qParamsNoRange}` @@ -182,6 +193,8 @@ export const OffChainDashboard: () => JSX.Element = () => { msgs, // Data data, + // Blobs + blobs, // Datatypes datatypes, ]: IGenericPagedResponse[] | any[]) => { @@ -190,6 +203,8 @@ export const OffChainDashboard: () => JSX.Element = () => { setMsgCount(msgs.total); // Data Count setDataCount(data.total); + // Blob count + setBlobCount(blobs.total); // Datatypes setDatatypesCount(datatypes.total); } @@ -200,7 +215,7 @@ export const OffChainDashboard: () => JSX.Element = () => { }); }, [selectedNamespace, dateFilter, lastRefreshTime, isMounted]); - const dataHeaders = [t('nameOrID'), t('created'), t('download')]; + const dataHeaders = [t('nameOrID'), t('created'), t('blob')]; const dataRecords: IDataTableRecord[] | undefined = data?.map((data) => ({ key: data.id, columns: [ @@ -215,20 +230,17 @@ export const OffChainDashboard: () => JSX.Element = () => { value: , }, { - value: data.blob && ( - { - e.stopPropagation(); - downloadBlobFile(data.id, data.blob?.name); - }} - > - - + value: data.blob ? ( + + ) : ( + ), }, ], - onClick: () => - navigate(FF_NAV_PATHS.offchainDataPath(selectedNamespace, data.id)), + onClick: () => { + setViewData(data); + setSlideSearchParam(data.id); + }, })); const dtHeaders = [t('id'), t('version'), t('created')]; @@ -243,8 +255,10 @@ export const OffChainDashboard: () => JSX.Element = () => { value: , }, ], - onClick: () => - navigate(FF_NAV_PATHS.offchainDatatypesPath(selectedNamespace, dt.id)), + onClick: () => { + setViewDatatype(dt); + setSlideSearchParam(dt.id); + }, })); const mediumCards: IFireFlyCard[] = [ @@ -440,22 +454,7 @@ export const OffChainDashboard: () => JSX.Element = () => { ), }, { - value: ( - // TODO: Fix when https://github.com/hyperledger/firefly/issues/628 is resolved - - ), + value: , }, ], onClick: () => { @@ -487,7 +486,9 @@ export const OffChainDashboard: () => JSX.Element = () => { return ( JSX.Element = () => { justifyContent="center" container item - xs={4} + md={12} + lg={4} > @@ -550,6 +552,26 @@ export const OffChainDashboard: () => JSX.Element = () => { /> + {viewData && ( + { + setViewData(undefined); + setSlideSearchParam(null); + }} + /> + )} + {viewDatatype && ( + { + setViewDatatype(undefined); + setSlideSearchParam(null); + }} + /> + )} {viewMsg && ( JSX.Element = () => { @@ -154,14 +154,7 @@ export const OffChainData: () => JSX.Element = () => { }, { value: d.blob && ( - { - e.stopPropagation(); - downloadBlobFile(d.id, d.blob?.name); - }} - > - - + ), }, ], @@ -182,7 +175,6 @@ export const OffChainData: () => JSX.Element = () => { ) => diff --git a/src/pages/Off-Chain/views/DataTypes.tsx b/src/pages/Off-Chain/views/DataTypes.tsx index 23641d58..2f1ed5de 100644 --- a/src/pages/Off-Chain/views/DataTypes.tsx +++ b/src/pages/Off-Chain/views/DataTypes.tsx @@ -166,7 +166,6 @@ export const OffChainDataTypes: () => JSX.Element = () => { ) => diff --git a/src/pages/Off-Chain/views/Messages.tsx b/src/pages/Off-Chain/views/Messages.tsx index 3d639d8c..1c02e7a6 100644 --- a/src/pages/Off-Chain/views/Messages.tsx +++ b/src/pages/Off-Chain/views/Messages.tsx @@ -14,12 +14,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Box, Chip, Grid } from '@mui/material'; +import { Box, Grid } from '@mui/material'; import { BarDatum } from '@nivo/bar'; import dayjs from 'dayjs'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Histogram } from '../../../components/Charts/Histogram'; +import { MsgStatusChip } from '../../../components/Chips/MsgStatusChip'; import { FilterButton } from '../../../components/Filters/FilterButton'; import { FilterModal } from '../../../components/Filters/FilterModal'; import { Header } from '../../../components/Header'; @@ -43,10 +44,7 @@ import { IPagedMessageResponse, MessageFilters, } from '../../../interfaces'; -import { - FF_MESSAGES_CATEGORY_MAP, - MsgStateColorMap, -} from '../../../interfaces/enums'; +import { FF_MESSAGES_CATEGORY_MAP } from '../../../interfaces/enums'; import { FF_TX_CATEGORY_MAP } from '../../../interfaces/enums/transactionTypes'; import { DEFAULT_HIST_HEIGHT, @@ -224,22 +222,7 @@ export const OffChainMessages: () => JSX.Element = () => { ), }, { - value: ( - // TODO: Fix when https://github.com/hyperledger/firefly/issues/628 is resolved - - ), + value: , }, ], onClick: () => { @@ -260,7 +243,6 @@ export const OffChainMessages: () => JSX.Element = () => { ) => diff --git a/src/pages/Tokens/Routes.tsx b/src/pages/Tokens/Routes.tsx index e493dd2f..3d79e125 100644 --- a/src/pages/Tokens/Routes.tsx +++ b/src/pages/Tokens/Routes.tsx @@ -1,5 +1,6 @@ import { RouteObject } from 'react-router-dom'; import { NAMESPACES_PATH } from '../../interfaces'; +import { TokensBalances } from './views/Balances'; import { TokensDashboard } from './views/Dashboard'; import { PoolDetails } from './views/PoolDetails'; import { TokensPools } from './views/Pools'; @@ -25,5 +26,9 @@ export const TokensRoutes: RouteObject = { path: 'pools/:poolID', element: , }, + { + path: 'balances', + element: , + }, ], }; diff --git a/src/pages/Tokens/views/Balances.tsx b/src/pages/Tokens/views/Balances.tsx new file mode 100644 index 00000000..54ce810c --- /dev/null +++ b/src/pages/Tokens/views/Balances.tsx @@ -0,0 +1,225 @@ +// 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 { FilterButton } from '../../../components/Filters/FilterButton'; +import { FilterModal } from '../../../components/Filters/FilterModal'; +import { Header } from '../../../components/Header'; +import { ChartTableHeader } from '../../../components/Headers/ChartTableHeader'; +import { HashPopover } from '../../../components/Popovers/HashPopover'; +import { BalanceSlide } from '../../../components/Slides/BalanceSlide'; +import { FFTableText } from '../../../components/Tables/FFTableText'; +import { DataTable } from '../../../components/Tables/Table'; +import { ApplicationContext } from '../../../contexts/ApplicationContext'; +import { DateFilterContext } from '../../../contexts/DateFilterContext'; +import { FilterContext } from '../../../contexts/FilterContext'; +import { SlideContext } from '../../../contexts/SlideContext'; +import { SnackbarContext } from '../../../contexts/SnackbarContext'; +import { + BalanceFilters, + FF_Paths, + IDataTableRecord, + IPagedTokenBalanceResponse, + ITokenBalance, +} from '../../../interfaces'; +import { DEFAULT_PADDING, DEFAULT_PAGE_LIMITS } from '../../../theme'; +import { fetchCatcher, getFFTime } from '../../../utils'; +import { hasTransferEvent } from '../../../utils/wsEvents'; + +export const KEY_POOL_DELIM = '||'; + +export const TokensBalances: () => JSX.Element = () => { + const { newEvents, lastRefreshTime, clearNewEvents, selectedNamespace } = + useContext(ApplicationContext); + const { dateFilter } = useContext(DateFilterContext); + const { filterAnchor, setFilterAnchor, filterString } = + useContext(FilterContext); + const { slideID, setSlideSearchParam } = useContext(SlideContext); + const { reportFetchError } = useContext(SnackbarContext); + const { t } = useTranslation(); + const [isMounted, setIsMounted] = useState(false); + // Token balances + const [tokenBalances, setTokenBalances] = useState(); + // Token balances totals + const [tokenBalancesTotal, setTokenBalancesTotal] = useState(0); + const [viewBalance, setViewBalance] = useState(); + const [currentPage, setCurrentPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_PAGE_LIMITS[1]); + + useEffect(() => { + setIsMounted(true); + return () => { + setIsMounted(false); + }; + }, []); + + useEffect(() => { + if (isMounted && slideID) { + // Expected structure: || + const keyPoolArray = slideID.split(KEY_POOL_DELIM); + if (keyPoolArray.length !== 2) { + return; + } + + fetchCatcher( + `${ + FF_Paths.nsPrefix + }/${selectedNamespace}${FF_Paths.tokenBalancesByKeyPool( + keyPoolArray[0], + keyPoolArray[1] + )}` + ) + .then((balanceRes: ITokenBalance[]) => { + isMounted && balanceRes.length === 1 && setViewBalance(balanceRes[0]); + }) + .catch((err) => { + reportFetchError(err); + }); + } + }, [slideID, isMounted]); + + // Token balances + useEffect(() => { + isMounted && + dateFilter && + fetchCatcher( + `${FF_Paths.nsPrefix}/${selectedNamespace}${ + FF_Paths.tokenBalances + }?limit=${rowsPerPage}&count&skip=${rowsPerPage * currentPage}${ + dateFilter.filterString + }${filterString ?? ''}` + ) + .then((tokenBalancesRes: IPagedTokenBalanceResponse) => { + setTokenBalances(tokenBalancesRes.items); + setTokenBalancesTotal(tokenBalancesRes.total); + }) + .catch((err) => { + reportFetchError(err); + }); + }, [ + rowsPerPage, + currentPage, + selectedNamespace, + dateFilter, + filterString, + lastRefreshTime, + isMounted, + ]); + + const tokenBalanceColHeaders = [ + t('key'), + t('balance'), + t('pool'), + t('uri'), + t('connector'), + t('updates'), + ]; + const tokenBalanceRecords: IDataTableRecord[] | undefined = + tokenBalances?.map((balance, idx) => ({ + key: idx.toString(), + columns: [ + { + value: , + }, + { + value: , + }, + { + value: , + }, + { + value: , + }, + { + value: , + }, + { + value: ( + + ), + }, + ], + onClick: () => { + setViewBalance(balance); + // Since a key can have transfers in multiple pools, the slide ID must be a string + // with the following structure: || + setSlideSearchParam([balance.key, balance.pool].join(KEY_POOL_DELIM)); + }, + })); + + return ( + <> +
+ + + ) => + setFilterAnchor(e.currentTarget) + } + /> + } + /> + + setCurrentPage(currentPage) + } + onHandleRowsPerPage={(rowsPerPage: number) => + setRowsPerPage(rowsPerPage) + } + stickyHeader={true} + minHeight="300px" + maxHeight="calc(100vh - 340px)" + records={tokenBalanceRecords} + columnHeaders={tokenBalanceColHeaders} + paginate={true} + emptyStateText={t('noTokenBalancesToDisplay')} + dataTotal={tokenBalancesTotal} + currentPage={currentPage} + rowsPerPage={rowsPerPage} + /> + + + {filterAnchor && ( + { + setFilterAnchor(null); + }} + fields={BalanceFilters} + /> + )} + {viewBalance && ( + { + setViewBalance(undefined); + setSlideSearchParam(null); + }} + /> + )} + + ); +}; diff --git a/src/pages/Tokens/views/Dashboard.tsx b/src/pages/Tokens/views/Dashboard.tsx index 18650fef..a8a2ec6d 100644 --- a/src/pages/Tokens/views/Dashboard.tsx +++ b/src/pages/Tokens/views/Dashboard.tsx @@ -15,7 +15,7 @@ // limitations under the License. import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import { Chip, Grid, IconButton } from '@mui/material'; +import { Grid, IconButton } from '@mui/material'; import { BarDatum } from '@nivo/bar'; import dayjs from 'dayjs'; import React, { useContext, useEffect, useState } from 'react'; @@ -27,6 +27,7 @@ import { SmallCard } from '../../../components/Cards/SmallCard'; import { Histogram } from '../../../components/Charts/Histogram'; import { Header } from '../../../components/Header'; import { HashPopover } from '../../../components/Popovers/HashPopover'; +import { BalanceSlide } from '../../../components/Slides/BalanceSlide'; import { TransferSlide } from '../../../components/Slides/TransferSlide'; import { FFTableText } from '../../../components/Tables/FFTableText'; import { MediumCardTable } from '../../../components/Tables/MediumCardTable'; @@ -36,6 +37,7 @@ import { DateFilterContext } from '../../../contexts/DateFilterContext'; import { SlideContext } from '../../../contexts/SlideContext'; import { SnackbarContext } from '../../../contexts/SnackbarContext'; import { + BALANCES_PATH, BucketCollectionEnum, BucketCountEnum, FF_NAV_PATHS, @@ -54,7 +56,6 @@ import { } from '../../../interfaces'; import { FF_TRANSFER_CATEGORY_MAP, - PoolStateColorMap, TransferIconMap, } from '../../../interfaces/enums'; import { @@ -70,6 +71,7 @@ import { } from '../../../utils/charts'; import { makeTransferHistogram } from '../../../utils/histograms/transferHistogram'; import { hasTransferEvent } from '../../../utils/wsEvents'; +import { KEY_POOL_DELIM } from './Balances'; export const TokensDashboard: () => JSX.Element = () => { const { t } = useTranslation(); @@ -86,8 +88,8 @@ export const TokensDashboard: () => JSX.Element = () => { const [tokenMintCount, setTokenMintcount] = useState(); const [tokenBurnCount, setTokenBurnCount] = useState(); const [tokenErrorCount, setTokenErrorCount] = useState(0); - // Accounts - const [tokenAccountsCount, setTokenAccountsCount] = useState(); + // Approvals + const [tokenApprovalCount, setTokenApprovalCount] = useState(); // Pools const [tokenPoolCount, setTokenPoolCount] = useState(); const [tokenPoolErrorCount, setTokenPoolErrorCount] = useState(0); @@ -109,6 +111,7 @@ export const TokensDashboard: () => JSX.Element = () => { const [viewTransfer, setViewTransfer] = useState< ITokenTransfer | undefined >(); + const [viewBalance, setViewBalance] = useState(); const [currentPage, setCurrentPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(DEFAULT_PAGE_LIMITS[0]); @@ -122,25 +125,43 @@ export const TokensDashboard: () => JSX.Element = () => { }, []); useEffect(() => { - isMounted && - slideID && - fetchCatcher( - `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.tokenTransferById( - slideID - )}` - ) - .then((transferRes: ITokenTransfer) => { + if (isMounted && slideID) { + // Expected structure: || + const keyPoolArray = slideID.split(KEY_POOL_DELIM); + if (keyPoolArray.length !== 2) { + fetchCatcher( + `${ + FF_Paths.nsPrefix + }/${selectedNamespace}${FF_Paths.tokenTransferById(slideID)}` + ).then((transferRes: ITokenTransfer) => { setViewTransfer(transferRes); - }) - .catch((err) => { - reportFetchError(err); }); + } else { + fetchCatcher( + `${ + FF_Paths.nsPrefix + }/${selectedNamespace}${FF_Paths.tokenBalancesByKeyPool( + keyPoolArray[0], + keyPoolArray[1] + )}` + ) + .then((balanceRes: ITokenBalance[]) => { + isMounted && + balanceRes.length === 1 && + setViewBalance(balanceRes[0]); + }) + .catch((err) => { + reportFetchError(err); + }); + } + } }, [slideID, isMounted]); const smallCards: ISmallCard[] = [ { - header: t('tokens'), + header: t('activity'), numErrors: tokenErrorCount, + errorLink: FF_NAV_PATHS.tokensTransfersErrorPath(selectedNamespace), data: [ { header: t('transfers'), data: tokenTransfersCount }, { header: t('mint'), data: tokenMintCount }, @@ -148,18 +169,17 @@ export const TokensDashboard: () => JSX.Element = () => { ], clickPath: TRANSFERS_PATH, }, - { - header: t('accounts'), - numErrors: 0, - data: [{ header: t('total'), data: tokenAccountsCount }], - clickPath: POOLS_PATH, - }, { header: t('tokenPools'), numErrors: tokenPoolErrorCount, data: [{ header: t('total'), data: tokenPoolCount }], clickPath: POOLS_PATH, }, + { + header: t('approvals'), + numErrors: 0, + data: [{ header: t('total'), data: tokenApprovalCount }], + }, { header: t('connectors'), numErrors: 0, @@ -187,9 +207,9 @@ export const TokensDashboard: () => JSX.Element = () => { fetchCatcher( `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.operations}${qParams}&type=token_create_pool&type=token_activate_pool&type=token_transfer&status=Failed` ), - // Accounts + // Approvals fetchCatcher( - `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.tokenAccounts}${qParams}` + `${FF_Paths.nsPrefix}/${selectedNamespace}${FF_Paths.tokenApprovals}${qParams}` ), // Pools fetchCatcher( @@ -210,8 +230,8 @@ export const TokensDashboard: () => JSX.Element = () => { tokensMint, tokensBurn, tokenErrors, - // Accounts - tokenAccounts, + // Approvals + tokenApprovals, // Pools tokenPools, tokenPoolErrors, @@ -224,8 +244,8 @@ export const TokensDashboard: () => JSX.Element = () => { setTokenMintcount(tokensMint.total); setTokenBurnCount(tokensBurn.total); setTokenErrorCount(tokenErrors.total); - // Accounts - setTokenAccountsCount(tokenAccounts.total); + // Approvals + setTokenApprovalCount(tokenApprovals.total); // Pools setTokenPoolCount(tokenPools.total); setTokenPoolErrorCount(tokenPoolErrors.total); @@ -254,26 +274,22 @@ export const TokensDashboard: () => JSX.Element = () => { value: , }, ], - onClick: () => - navigate( - FF_NAV_PATHS.tokensPoolDetailsPath(selectedNamespace, acct.pool) - ), + onClick: () => { + setViewBalance(acct); + // Since a key can have transfers in multiple pools, the slide ID must be a string + // with the following structure: || + setSlideSearchParam([acct.key, acct.pool].join(KEY_POOL_DELIM)); + }, })); - const tokenPoolColHeaders = [t(''), t('name'), t('standard'), t('state')]; + const tokenPoolColHeaders = [t('name'), t('symbol'), t('standard')]; const tokenPoolRecords: IDataTableRecord[] | undefined = tokenPools?.map( (pool) => ({ key: pool.id, columns: [ - { - value: ( - - ), - }, { value: ( 10 ? ( @@ -282,18 +298,20 @@ export const TokensDashboard: () => JSX.Element = () => { pool.name ) } + icon={ + + } /> ), }, - { value: }, { - value: pool.state && ( - + value: pool.symbol ? ( + + ) : ( + ), }, + { value: }, ], onClick: () => navigate( @@ -327,7 +345,7 @@ export const TokensDashboard: () => JSX.Element = () => { { headerText: t('accountBalances'), headerComponent: ( - navigate(POOLS_PATH)}> + navigate(BALANCES_PATH)}> ), @@ -406,7 +424,7 @@ export const TokensDashboard: () => JSX.Element = () => { t('to'), t('amount'), t('blockchainEvent'), - t('author'), + t('signingKey'), t('timestamp'), ]; const tokenTransferRecords: IDataTableRecord[] | undefined = @@ -442,12 +460,7 @@ export const TokensDashboard: () => JSX.Element = () => { value: , }, { - value: ( - - ), + value: , }, { value: ( @@ -518,7 +531,9 @@ export const TokensDashboard: () => JSX.Element = () => { return ( JSX.Element = () => { justifyContent="center" container item - xs={4} + md={12} + lg={4} > @@ -591,6 +607,16 @@ export const TokensDashboard: () => JSX.Element = () => { }} /> )} + {viewBalance && ( + { + setViewBalance(undefined); + setSlideSearchParam(null); + }} + /> + )} ); }; diff --git a/src/pages/Tokens/views/PoolDetails.tsx b/src/pages/Tokens/views/PoolDetails.tsx index 624cfa7f..989fdda3 100644 --- a/src/pages/Tokens/views/PoolDetails.tsx +++ b/src/pages/Tokens/views/PoolDetails.tsx @@ -200,6 +200,17 @@ export const PoolDetails: () => JSX.Element = () => { const accountsCard = { headerText: t('accountsInPool'), + headerComponent: pool && ( + + navigate( + FF_NAV_PATHS.tokensBalancesPathByPool(selectedNamespace, pool.id) + ) + } + > + + + ), component: ( JSX.Element = () => { t('to'), t('amount'), t('blockchainEvent'), - t('author'), + t('signingKey'), t('timestamp'), ]; const tokenTransferRecords: IDataTableRecord[] | undefined = @@ -327,7 +338,7 @@ export const PoolDetails: () => JSX.Element = () => { {pool.name}
- +
)} diff --git a/src/pages/Tokens/views/Pools.tsx b/src/pages/Tokens/views/Pools.tsx index 62ee0a2f..b86c62f7 100644 --- a/src/pages/Tokens/views/Pools.tsx +++ b/src/pages/Tokens/views/Pools.tsx @@ -14,11 +14,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Chip, Grid } from '@mui/material'; +import { Grid } from '@mui/material'; import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Jazzicon from 'react-jazzicon'; import { useNavigate } from 'react-router-dom'; +import { PoolStatusChip } from '../../../components/Chips/PoolStatusChip'; import { FilterButton } from '../../../components/Filters/FilterButton'; import { FilterModal } from '../../../components/Filters/FilterModal'; import { Header } from '../../../components/Header'; @@ -37,7 +38,6 @@ import { IPagedTokenPoolResponse, ITokenPool, PoolFilters, - PoolStateColorMap, } from '../../../interfaces'; import { DEFAULT_PADDING, DEFAULT_PAGE_LIMITS } from '../../../theme'; import { fetchCatcher, getFFTime, jsNumberForAddress } from '../../../utils'; @@ -135,7 +135,7 @@ export const TokensPools: () => JSX.Element = () => { value: pool.symbol ? ( ) : ( - + ), }, { @@ -151,12 +151,7 @@ export const TokensPools: () => JSX.Element = () => { value: , }, { - value: ( - - ), + value: , }, { value: ( @@ -182,7 +177,6 @@ export const TokensPools: () => JSX.Element = () => { ) => diff --git a/src/pages/Tokens/views/Transfers.tsx b/src/pages/Tokens/views/Transfers.tsx index 301f5042..d63100c1 100644 --- a/src/pages/Tokens/views/Transfers.tsx +++ b/src/pages/Tokens/views/Transfers.tsx @@ -204,12 +204,7 @@ export const TokensTransfers: () => JSX.Element = () => { value: , }, { - value: ( - - ), + value: , }, { value: ( @@ -240,7 +235,6 @@ export const TokensTransfers: () => JSX.Element = () => { ) => diff --git a/src/translations/en.json b/src/translations/en.json index 06e300bc..e7ee679b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6,32 +6,18 @@ "activity": "Activity", "addFilter": "Add Filter", "address": "Address", - "allAccounts": "All Accounts", - "allActivity": "All Activity", - "allApis": "All APIs", - "allBlockchainEvents": "All Blockchain Events", - "allData": "All Data", - "allDatatypes": "All Datatypes", - "allDurableSubscriptions": "All Durable Subscriptions", - "allEvents": "All Events", - "allIdentities": "All Identities", - "allInterfaces": "All Interfaces", - "allListeners": "All Listeners", - "allMessages": "All Messages", - "allNodes": "All Nodes", - "allOperations": "All Operations", - "allOrganizations": "All Organizations", - "allPools": "All Pools", - "allTransactions": "All Transactions", - "allTransfers": "All Transfers", "amount": "Amount", "api": "API", "apis": "APIs", + "approvals": "Approvals", "author": "Author", "authorKey": "Author Key", + "balance": "Balance", + "balances": "Balances", "batch": "Batch", "batchPin": "Batch Pin", "blobName": "Blob Name", + "blobs": "Blobs", "blobSize": "Blob Size", "blockchain": "Blockchain", "blockchainBatchPin": "Blockchain Batch Pin", @@ -54,6 +40,7 @@ "connector": "Connector", "connectors": "Connectors", "contains": "Contains", + "contractAPI": "Contract API", "contractApiConfirmed": "Contract API Confirmed", "contractAPIs": "Contract APIs", "contractInterface": "Contract Interface", @@ -94,11 +81,13 @@ "eventName": "Event Name", "eventParams": "Event Params", "events": "Events", + "eventSchema": "Event Schema", "eventTypes": "Event Types", "failed": "Failed", "field": "Field", "fileExplorer": "File Explorer", "filter": "Filter", + "fireflyID": "FireFly ID", "from": "From", "greaterThan": "Greater than", "greaterThanOrEqual": "Greater than or equal", @@ -152,6 +141,7 @@ "noActivity": "No Activity", "noAddressForListener": "No Address for Listener", "noApisToDisplay": "No APIs to Display", + "noBlobInData": "No Blob in Data", "noBlobName": "No Blob Name", "noBlockchainEvents": "No Blockchain Events to Display", "noBlockchainIds": "No Blockchain IDs", @@ -187,16 +177,17 @@ "noRecentNetworkEvents": "No Recent Network Events", "noRecentTransactions": "No Recent Transactions", "noSubscriptionsToDisplay": "No Subscriptions to Display", - "noSymbolSpecified": "No Symbol Specified", "noTagInMessage": "No Tag in Message", "noTimelineEvents": "No Timeline Events to Display", "notMatches": "Does not match", "noToAccount": "No To Account", "noTokenAccounts": "No Token Accounts", + "noTokenBalancesToDisplay": "No Token Balances to Display", "noTokenPools": "No Token Pools", "noTokenPoolsToDisplay": "No Token Pools to Display", "noTokenTransfersInPool": "No Transfers in Pool", "noTokenTransfersToDisplay": "No Token Transfers to Display", + "noTopicInListener": "No Topic in Listener", "noTopicInMessage": "No Topic in Message", "noTransactions": "No Transactions", "noTransactionsToDisplay": "No Transactions to Display", @@ -239,6 +230,8 @@ "reference": "Reference", "referenceID": "Reference ID", "refresh": "Refresh", + "retried": "Retried", + "retriedOperation": "Retried Operation", "rule": "Rule", "save": "Save", "schema": "Schema", @@ -251,6 +244,7 @@ "sharedStorageDownloadBlob": "Shared Storage Download Blob", "sharedStorageUploadBatch": "Shared Storage Upload Batch", "sharedStorageUploadBlob": "Shared Storage Upload Blob", + "signingKey": "Signing Key", "source": "Source", "standard": "Standard", "startsWith": "Starts with", @@ -260,7 +254,6 @@ "submittedByMe": "Submitted by Me", "subscription": "Subscription", "subscriptions": "Subscriptions", - "swagger": "Swagger", "tag": "Tag", "timeline": "Timeline", "timestamp": "Timestamp", @@ -279,6 +272,7 @@ "tokenTransferConfirmed": "Token Transfer Confirmed", "tokenTransferFailed": "Token Transfer Failed", "tokenTransferTypes": "Token Transfer Types", + "topic": "Topic", "topics": "Topics", "total": "Total", "totalDatatypes": "Total Datatypes", @@ -302,6 +296,7 @@ "ui": "UI", "unpinned": "Unpinned", "updated": "Updated", + "uri": "URI", "validator": "Validator", "value": "Value", "version": "Version", diff --git a/src/utils/files.ts b/src/utils/files.ts index eaabf373..d7582ef2 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -16,3 +16,18 @@ export const downloadBlobFile = async (id: string, filename?: string) => { link.click(); document.body.removeChild(link); }; + +export const downloadExternalFile = async (url: string, filename?: string) => { + const file = await fetch(url); + const blob = await file.blob(); + const href = await URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + if (filename) { + link.download = filename; + } + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 274621a8..150607d5 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -22,11 +22,3 @@ export const makeMultipleQueryParams = ( .toString(); return `&${queryKey}=${str.replaceAll(',', `&${queryKey}=`)}`; }; - -export const isValidUUID = (str: string | undefined | null): boolean => { - return str - ? /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi.test( - str - ) - : false; -}; diff --git a/src/utils/wsEvents.tsx b/src/utils/wsEvents.tsx index 120ae29c..3d3029e2 100644 --- a/src/utils/wsEvents.tsx +++ b/src/utils/wsEvents.tsx @@ -1,65 +1,81 @@ -import { FF_EVENTS } from '../interfaces'; +import { FF_EVENTS, INewEventSet } from '../interfaces'; -export const hasApiEvent = (events: FF_EVENTS[]) => { - return events.includes(FF_EVENTS.CONTRACT_API_CONFIRMED); -}; - -export const hasBlockchainEvent = (events: FF_EVENTS[]) => { +export const hasAnyEvent = (events: INewEventSet) => { return ( - events.includes(FF_EVENTS.BLOCKCHAIN_EVENT_RECEIVED) || - events.includes(FF_EVENTS.CONTRACT_API_CONFIRMED) || - events.includes(FF_EVENTS.CONTRACT_INTERFACE_CONFIRMED) || - events.includes(FF_EVENTS.DATATYPE_CONFIRMED) || - events.includes(FF_EVENTS.IDENTITY_CONFIRMED) || - events.includes(FF_EVENTS.IDENTITY_UPDATED) || - events.includes(FF_EVENTS.NS_CONFIRMED) + events[FF_EVENTS.BLOCKCHAIN_EVENT_RECEIVED] || + events[FF_EVENTS.CONTRACT_API_CONFIRMED] || + events[FF_EVENTS.CONTRACT_INTERFACE_CONFIRMED] || + events[FF_EVENTS.DATATYPE_CONFIRMED] || + events[FF_EVENTS.IDENTITY_CONFIRMED] || + events[FF_EVENTS.IDENTITY_UPDATED] || + events[FF_EVENTS.NS_CONFIRMED] || + events[FF_EVENTS.MSG_CONFIRMED] || + events[FF_EVENTS.MSG_REJECTED] || + events[FF_EVENTS.TX_SUBMITTED] || + events[FF_EVENTS.TOKEN_POOL_CONFIRMED] || + events[FF_EVENTS.TOKEN_APPROVAL_CONFIRMED] || + events[FF_EVENTS.TOKEN_APPROVAL_OP_FAILED] || + events[FF_EVENTS.TOKEN_TRANSFER_CONFIRMED] || + events[FF_EVENTS.TOKEN_TRANSFER_FAILED] ); }; -export const hasDatatypeEvent = (events: FF_EVENTS[]) => { - return events.includes(FF_EVENTS.DATATYPE_CONFIRMED); +export const hasApiEvent = (events: INewEventSet) => { + return events[FF_EVENTS.CONTRACT_API_CONFIRMED]; }; -export const hasDataEvent = (events: FF_EVENTS[]) => { +export const hasBlockchainEvent = (events: INewEventSet) => { return ( - events.includes(FF_EVENTS.MSG_CONFIRMED) || - events.includes(FF_EVENTS.MSG_REJECTED) + events[FF_EVENTS.BLOCKCHAIN_EVENT_RECEIVED] || + events[FF_EVENTS.CONTRACT_API_CONFIRMED] || + events[FF_EVENTS.CONTRACT_INTERFACE_CONFIRMED] || + events[FF_EVENTS.DATATYPE_CONFIRMED] || + events[FF_EVENTS.IDENTITY_CONFIRMED] || + events[FF_EVENTS.IDENTITY_UPDATED] || + events[FF_EVENTS.NS_CONFIRMED] ); }; -export const hasIdentityEvent = (events: FF_EVENTS[]) => { +export const hasDatatypeEvent = (events: INewEventSet) => { + return events[FF_EVENTS.DATATYPE_CONFIRMED]; +}; + +export const hasDataEvent = (events: INewEventSet) => { + return events[FF_EVENTS.MSG_CONFIRMED] || events[FF_EVENTS.MSG_REJECTED]; +}; + +export const hasIdentityEvent = (events: INewEventSet) => { return ( - events.includes(FF_EVENTS.IDENTITY_CONFIRMED) || - events.includes(FF_EVENTS.IDENTITY_UPDATED) + events[FF_EVENTS.IDENTITY_CONFIRMED] || events[FF_EVENTS.IDENTITY_UPDATED] ); }; -export const hasInterfaceEvent = (events: FF_EVENTS[]) => { - return events.includes(FF_EVENTS.CONTRACT_INTERFACE_CONFIRMED); +export const hasInterfaceEvent = (events: INewEventSet) => { + return events[FF_EVENTS.CONTRACT_INTERFACE_CONFIRMED]; }; -export const hasOffchainEvent = (events: FF_EVENTS[]) => { +export const hasOffchainEvent = (events: INewEventSet) => { return ( - events.includes(FF_EVENTS.MSG_CONFIRMED) || - events.includes(FF_EVENTS.MSG_REJECTED) || - events.includes(FF_EVENTS.DATATYPE_CONFIRMED) + events[FF_EVENTS.MSG_CONFIRMED] || + events[FF_EVENTS.MSG_REJECTED] || + events[FF_EVENTS.DATATYPE_CONFIRMED] ); }; -export const hasPoolEvent = (events: FF_EVENTS[]) => { - return events.includes(FF_EVENTS.TOKEN_POOL_CONFIRMED); +export const hasPoolEvent = (events: INewEventSet) => { + return events[FF_EVENTS.TOKEN_POOL_CONFIRMED]; }; -export const hasTransferEvent = (events: FF_EVENTS[]) => { +export const hasTransferEvent = (events: INewEventSet) => { return ( - events.includes(FF_EVENTS.TOKEN_POOL_CONFIRMED) || - events.includes(FF_EVENTS.TOKEN_APPROVAL_CONFIRMED) || - events.includes(FF_EVENTS.TOKEN_APPROVAL_OP_FAILED) || - events.includes(FF_EVENTS.TOKEN_TRANSFER_CONFIRMED) || - events.includes(FF_EVENTS.TOKEN_TRANSFER_FAILED) + events[FF_EVENTS.TOKEN_POOL_CONFIRMED] || + events[FF_EVENTS.TOKEN_APPROVAL_CONFIRMED] || + events[FF_EVENTS.TOKEN_APPROVAL_OP_FAILED] || + events[FF_EVENTS.TOKEN_TRANSFER_CONFIRMED] || + events[FF_EVENTS.TOKEN_TRANSFER_FAILED] ); }; -export const hasTxEvent = (events: FF_EVENTS[]) => { - return events.includes(FF_EVENTS.TX_SUBMITTED); +export const hasTxEvent = (events: INewEventSet) => { + return events[FF_EVENTS.TX_SUBMITTED]; };