From 94a0d0907e60bb0fbd072ded966d101f8eb497e1 Mon Sep 17 00:00:00 2001 From: David Echelberger Date: Mon, 28 Mar 2022 18:14:03 -0400 Subject: [PATCH] [subscriptions] subscriptions page Signed-off-by: David Echelberger --- src/components/Lists/SubList.tsx | 49 +++++ src/components/Navigation/MyNodeNav.tsx | 55 ++++++ src/components/Navigation/Navigation.tsx | 9 +- src/components/Slides/SubscriptionSlide.tsx | 69 +++++++ src/interfaces/api.ts | 18 ++ src/interfaces/filters.ts | 11 ++ src/interfaces/navigation.ts | 3 + src/pages/MyNode/Routes.tsx | 5 + src/pages/MyNode/views/Subscriptions.tsx | 196 ++++++++++++++++++++ src/translations/en.json | 6 + 10 files changed, 414 insertions(+), 7 deletions(-) create mode 100644 src/components/Lists/SubList.tsx create mode 100644 src/components/Navigation/MyNodeNav.tsx create mode 100644 src/components/Slides/SubscriptionSlide.tsx create mode 100644 src/pages/MyNode/views/Subscriptions.tsx diff --git a/src/components/Lists/SubList.tsx b/src/components/Lists/SubList.tsx new file mode 100644 index 00000000..4078226f --- /dev/null +++ b/src/components/Lists/SubList.tsx @@ -0,0 +1,49 @@ +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 { FFCircleLoader } from '../Loaders/FFCircleLoader'; +import { FFListItem } from './FFListItem'; +import { FFListText } from './FFListText'; +import { FFListTimestamp } from './FFListTimestamp'; +import { FFSkeletonList } from './FFSkeletonList'; + +interface Props { + sub?: ISubscription; +} + +export const SubList: React.FC = ({ sub }) => { + const { t } = useTranslation(); + const [dataList, setDataList] = useState(FFSkeletonList); + + useEffect(() => { + if (sub) { + setDataList([ + { + label: t('id'), + value: , + button: , + }, + { + label: t('transport'), + value: , + }, + { + label: t('created'), + value: , + }, + ]); + } + }, [sub]); + + return ( + <> + {!sub ? ( + + ) : ( + dataList.map((d, idx) => ) + )} + + ); +}; diff --git a/src/components/Navigation/MyNodeNav.tsx b/src/components/Navigation/MyNodeNav.tsx new file mode 100644 index 00000000..fa345489 --- /dev/null +++ b/src/components/Navigation/MyNodeNav.tsx @@ -0,0 +1,55 @@ +// 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 HexagonIcon from '@mui/icons-material/Hexagon'; +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { ApplicationContext } from '../../contexts/ApplicationContext'; +import { FF_NAV_PATHS, INavItem } from '../../interfaces'; +import { NavSection } from './NavSection'; + +export const MyNodeNav = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { selectedNamespace } = useContext(ApplicationContext); + const { pathname } = useLocation(); + + const myNodePath = FF_NAV_PATHS.myNodePath(selectedNamespace); + const myNodeSubscriptionsPath = + FF_NAV_PATHS.myNodeSubscriptionsPath(selectedNamespace); + + const navItems: INavItem[] = [ + { + name: t('dashboard'), + action: () => navigate(myNodePath), + itemIsActive: pathname === myNodePath, + }, + { + name: t('subscriptions'), + action: () => navigate(myNodeSubscriptionsPath), + itemIsActive: pathname === myNodeSubscriptionsPath, + }, + ]; + + return ( + } + navItems={navItems} + title={t('myNode')} + /> + ); +}; diff --git a/src/components/Navigation/Navigation.tsx b/src/components/Navigation/Navigation.tsx index b0293ce0..581e1c27 100644 --- a/src/components/Navigation/Navigation.tsx +++ b/src/components/Navigation/Navigation.tsx @@ -17,6 +17,7 @@ import { FF_NAV_PATHS } from '../../interfaces'; import { MenuLogo } from '../MenuLogo'; import { ActivityNav } from './ActivityNav'; import { BlockchainNav } from './BlockchainNav'; +import { MyNodeNav } from './MyNodeNav'; import { NavItem } from './NavItem'; import { NetworkNav } from './NetworkNav'; import { OffChainNav } from './OffChainNav'; @@ -45,13 +46,7 @@ export const Navigation: React.FC = () => { - } - action={() => navigate(FF_NAV_PATHS.myNodePath(selectedNamespace))} - itemIsActive={pathname === FF_NAV_PATHS.myNodePath(selectedNamespace)} - isRoot - /> + } diff --git a/src/components/Slides/SubscriptionSlide.tsx b/src/components/Slides/SubscriptionSlide.tsx new file mode 100644 index 00000000..b3c4be6e --- /dev/null +++ b/src/components/Slides/SubscriptionSlide.tsx @@ -0,0 +1,69 @@ +// 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 { ISubscription } from '../../interfaces'; +import { DEFAULT_PADDING } from '../../theme'; +import { JsonViewAccordion } from '../Accordions/JsonViewerAccordion'; +import { SubList } from '../Lists/SubList'; +import { DisplaySlide } from './DisplaySlide'; +import { SlideHeader } from './SlideHeader'; + +interface Props { + sub: ISubscription; + open: boolean; + onClose: () => void; +} + +export const SubscriptionSlide: React.FC = ({ sub, open, onClose }) => { + const { t } = useTranslation(); + + return ( + <> + + + {/* Header */} + + {/* Data list */} + + + + + {sub.filter && ( + + + + )} + {sub.options && ( + + + + )} + + + + ); +}; diff --git a/src/interfaces/api.ts b/src/interfaces/api.ts index c73de7e4..567db36c 100644 --- a/src/interfaces/api.ts +++ b/src/interfaces/api.ts @@ -342,6 +342,13 @@ export interface IPagedOrganizationResponse { total: number; } +export interface IPagedSubscriptionsResponse { + pageParam: number; + count: number; + items: ISubscription[]; + total: number; +} + export interface IPagedTokenPoolResponse { pageParam: number; count: number; @@ -380,6 +387,17 @@ export interface IStatus { }; } +export interface ISubscription { + id: string; + namespace: string; + name: string; + transport: string; + filter?: any; + options?: any; + created: string; + updated: string | null; +} + export interface ITokenAccount { key: string; } diff --git a/src/interfaces/filters.ts b/src/interfaces/filters.ts index 0c6f5c83..9dd220ca 100644 --- a/src/interfaces/filters.ts +++ b/src/interfaces/filters.ts @@ -140,6 +140,17 @@ export const PoolFilters = [ 'tx.id', ]; +export const SubscriptionFilters = [ + 'id', + 'namespace', + 'name', + 'transport', + 'events', + 'filters', + 'options', + 'created', +]; + export const TransactionFilters = ['id', 'type', 'created', 'blockchainids']; export const TransferFilters = [ diff --git a/src/interfaces/navigation.ts b/src/interfaces/navigation.ts index f2bdce9c..8d98d85c 100644 --- a/src/interfaces/navigation.ts +++ b/src/interfaces/navigation.ts @@ -42,6 +42,7 @@ export const OFFCHAIN_PATH = 'offChain'; export const OPERATIONS_PATH = 'operations'; export const ORGANIZATIONS_PATH = 'organizations'; export const POOLS_PATH = 'pools'; +export const SUBSCRIPTIONS_PATH = 'subscriptions'; export const TOKENS_PATH = 'tokens'; export const TRANSACTIONS_PATH = 'transactions'; export const TRANSFERS_PATH = 'transfers'; @@ -121,6 +122,8 @@ export const FF_NAV_PATHS = { `/${NAMESPACES_PATH}/${ns}/${NETWORK_PATH}/${IDENTITIES_PATH}`, // My Node myNodePath: (ns: string) => `/${NAMESPACES_PATH}/${ns}/${MY_NODES_PATH}`, + myNodeSubscriptionsPath: (ns: string) => + `/${NAMESPACES_PATH}/${ns}/${MY_NODES_PATH}/${SUBSCRIPTIONS_PATH}`, // Docs docsPath: DOCS_PATH, }; diff --git a/src/pages/MyNode/Routes.tsx b/src/pages/MyNode/Routes.tsx index 7df091d3..43947101 100644 --- a/src/pages/MyNode/Routes.tsx +++ b/src/pages/MyNode/Routes.tsx @@ -1,6 +1,7 @@ import { RouteObject } from 'react-router-dom'; import { NAMESPACES_PATH } from '../../interfaces'; import { MyNodeDashboard } from './views/Dashboard'; +import { MyNodeSubscriptions } from './views/Subscriptions'; export const MyNodeRoutes: RouteObject = { path: `${NAMESPACES_PATH}/:namespace/myNode`, @@ -10,5 +11,9 @@ export const MyNodeRoutes: RouteObject = { index: true, element: , }, + { + path: 'subscriptions', + element: , + }, ], }; diff --git a/src/pages/MyNode/views/Subscriptions.tsx b/src/pages/MyNode/views/Subscriptions.tsx new file mode 100644 index 00000000..4876e85f --- /dev/null +++ b/src/pages/MyNode/views/Subscriptions.tsx @@ -0,0 +1,196 @@ +// 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 { SubscriptionSlide } from '../../../components/Slides/SubscriptionSlide'; +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 { + FF_Paths, + IDataTableRecord, + IPagedSubscriptionsResponse, + ISubscription, + SubscriptionFilters, +} from '../../../interfaces'; +import { DEFAULT_PADDING, DEFAULT_PAGE_LIMITS } from '../../../theme'; +import { fetchCatcher, getFFTime } from '../../../utils'; + +export const MyNodeSubscriptions: () => JSX.Element = () => { + const { 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); + + const [subscriptions, setSubscriptions] = useState(); + const [subscriptionsTotal, setSubscriptionsTotal] = useState(0); + const [viewSub, setViewSub] = 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.nsPrefix}/${selectedNamespace}${FF_Paths.subscriptionsById( + slideID + )}` + ) + .then((subRes: ISubscription) => { + setViewSub(subRes); + }) + .catch((err) => { + reportFetchError(err); + }); + }, [slideID, isMounted]); + + // Subscriptions + useEffect(() => { + isMounted && + fetchCatcher( + `${FF_Paths.nsPrefix}/${selectedNamespace}${ + FF_Paths.subscriptions + }?limit=${rowsPerPage}&count&skip=${rowsPerPage * currentPage}${ + filterString ?? '' + }&sort=created` + ) + .then((subRes: IPagedSubscriptionsResponse) => { + if (isMounted) { + setSubscriptions(subRes.items); + setSubscriptionsTotal(subRes.total); + } + }) + .catch((err) => { + reportFetchError(err); + }); + }, [rowsPerPage, currentPage, selectedNamespace, filterString, isMounted]); + + const subColHeaders = [t('name'), t('id'), t('transport'), t('created')]; + const subRecords: IDataTableRecord[] | undefined = subscriptions?.map( + (sub) => { + return { + key: sub.id, + columns: [ + { + value: , + }, + { + value: , + }, + { + value: , + }, + { + value: ( + + ), + }, + ], + onClick: () => { + setViewSub(sub); + setSlideSearchParam(sub.id); + }, + }; + } + ); + + return ( + <> +
+ + + ) => + setFilterAnchor(e.currentTarget) + } + /> + } + /> + + setCurrentPage(currentPage) + } + onHandleRowsPerPage={(rowsPerPage: number) => + setRowsPerPage(rowsPerPage) + } + stickyHeader={true} + minHeight="300px" + maxHeight="calc(100vh - 340px)" + records={subRecords} + columnHeaders={subColHeaders} + paginate={true} + emptyStateText={t('noSubscriptionsToDisplay')} + dataTotal={subscriptionsTotal} + currentPage={currentPage} + rowsPerPage={rowsPerPage} + /> + + + {filterAnchor && ( + { + setFilterAnchor(null); + }} + fields={SubscriptionFilters} + /> + )} + {viewSub && ( + { + setViewSub(undefined); + setSlideSearchParam(null); + }} + /> + )} + + ); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 87e8d117..06e300bc 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -12,6 +12,7 @@ "allBlockchainEvents": "All Blockchain Events", "allData": "All Data", "allDatatypes": "All Datatypes", + "allDurableSubscriptions": "All Durable Subscriptions", "allEvents": "All Events", "allIdentities": "All Identities", "allInterfaces": "All Interfaces", @@ -97,6 +98,7 @@ "failed": "Failed", "field": "Field", "fileExplorer": "File Explorer", + "filter": "Filter", "from": "From", "greaterThan": "Greater than", "greaterThanOrEqual": "Greater than or equal", @@ -184,6 +186,7 @@ "noRecentData": "No Recent Data", "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", @@ -205,6 +208,7 @@ "operationID": "Operation ID", "operations": "Operations", "operator": "Operator", + "options": "Options", "organization": "Organization", "organizations": "Organizations", "orgID": "Org ID", @@ -254,6 +258,7 @@ "status": "Status", "structuredData": "Structured Data", "submittedByMe": "Submitted by Me", + "subscription": "Subscription", "subscriptions": "Subscriptions", "swagger": "Swagger", "tag": "Tag", @@ -290,6 +295,7 @@ "transferPrivate": "Transfer Private", "transfers": "Transfers", "transfersInPool": "Transfers In Pool", + "transport": "Transport", "tx": "Tx", "txHash": "Tx Hash", "type": "Type",