From 7555c6d4579bd255fbd1ad296f58a15ebc99092b Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Mon, 27 Jan 2025 10:31:26 -0500 Subject: [PATCH 01/10] Key explorer UI Signed-off-by: Gabriel Indik --- ui/client/src/App.tsx | 4 +- ui/client/src/components/Hash.tsx | 8 +- ui/client/src/components/Header.tsx | 8 +- ui/client/src/interfaces.ts | 11 +++ ui/client/src/queries/keys.ts | 45 +++++++++ ui/client/src/queries/rpcMethods.ts | 3 +- ui/client/src/routes.ts | 1 + ui/client/src/translations/en.json | 10 +- ui/client/src/views/Keys.tsx | 142 ++++++++++++++++++++++++++++ 9 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 ui/client/src/queries/keys.ts create mode 100644 ui/client/src/views/Keys.tsx diff --git a/ui/client/src/App.tsx b/ui/client/src/App.tsx index c4c04afe3..6aa86f626 100644 --- a/ui/client/src/App.tsx +++ b/ui/client/src/App.tsx @@ -26,13 +26,14 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { Header } from "./components/Header"; import { ApplicationContextProvider } from "./contexts/ApplicationContext"; import { darkThemeOptions, lightThemeOptions } from "./themes/default"; -import { Indexer } from "./views/indexer"; +import { Indexer } from "./views/Indexer"; import { Registries } from "./views/Registries"; import { Submissions } from "./views/Submissions"; import { useEffect, useMemo, useState } from "react"; import { constants } from "./components/config"; import { AppRoutes } from "./routes"; import { Nodes } from "./views/Peers"; +import { Keys } from "./views/Keys"; const queryClient = new QueryClient({ queryCache: new QueryCache({}), @@ -99,6 +100,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/ui/client/src/components/Hash.tsx b/ui/client/src/components/Hash.tsx index f34a69877..4aab37e57 100644 --- a/ui/client/src/components/Hash.tsx +++ b/ui/client/src/components/Hash.tsx @@ -1,4 +1,4 @@ -// Copyright © 2024 Kaleido, Inc. +// Copyright © 20245 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -23,10 +23,11 @@ const MAX_LENGTH_WITHOUT_COLLAPSE = 16; type Props = { Icon?: JSX.Element title: string + hideTitle?: boolean hash: string } -export const Hash: React.FC = ({ Icon, title, hash }) => { +export const Hash: React.FC = ({ Icon, title, hideTitle, hash }) => { const [hashDialogOpen, setHashDialogOpen] = useState(false); @@ -37,6 +38,7 @@ export const Hash: React.FC = ({ Icon, title, hash }) => { return `${value.substring(0, 5)}...${value.substring(value.length - 3)}` }; + const content = hideTitle ? getHash(hash) : `${title} | ${getHash(hash)}` return ( <> @@ -49,7 +51,7 @@ export const Hash: React.FC = ({ Icon, title, hash }) => { color="secondary" sx={{ paddingTop: 0, paddingBottom: 0, textTransform: 'none', fontWeight: '400', whiteSpace: 'nowrap' }} size="small"> - {`${title} | ${getHash(hash)}`} + {content} diff --git a/ui/client/src/components/Header.tsx b/ui/client/src/components/Header.tsx index bed41c84f..02a6209b7 100644 --- a/ui/client/src/components/Header.tsx +++ b/ui/client/src/components/Header.tsx @@ -41,8 +41,10 @@ export const Header: React.FC = () => { return 1; } else if (path.startsWith(AppRoutes.Peers)) { return 2; - } else if (path.startsWith(AppRoutes.Registry)) { + } else if (path.startsWith(AppRoutes.Keys)) { return 3; + }else if (path.startsWith(AppRoutes.Registry)) { + return 4; } return 0; }; @@ -55,7 +57,8 @@ export const Header: React.FC = () => { case 0: navigate(AppRoutes.Indexer); break; case 1: navigate(AppRoutes.Submissions); break; case 2: navigate(AppRoutes.Peers); break; - case 3: navigate(AppRoutes.Registry); break; + case 3: navigate(AppRoutes.Keys); break; + case 4: navigate(AppRoutes.Registry); break; } }; @@ -77,6 +80,7 @@ export const Header: React.FC = () => { + diff --git a/ui/client/src/interfaces.ts b/ui/client/src/interfaces.ts index c96d788f6..da49dcdca 100644 --- a/ui/client/src/interfaces.ts +++ b/ui/client/src/interfaces.ts @@ -145,3 +145,14 @@ export interface ITransportPeer { endpoint: string } } + +export interface IKeyEntry { + isKey: boolean + hasChildren: boolean + path: string + index: number + type: string + verifier: string + wallet: string + keyHandle: string +} diff --git a/ui/client/src/queries/keys.ts b/ui/client/src/queries/keys.ts new file mode 100644 index 000000000..bf2ff8d63 --- /dev/null +++ b/ui/client/src/queries/keys.ts @@ -0,0 +1,45 @@ +// Copyright © 2025 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 { IKeyEntry } from "../interfaces"; +import { generatePostReq, returnResponse } from "./common"; +import { RpcEndpoint, RpcMethods } from "./rpcMethods"; +import i18next from "i18next"; + +export const fetchKeys = async (parent: string): Promise => { + const requestPayload = { + jsonrpc: "2.0", + id: Date.now(), + method: RpcMethods.keymgr_queryKeys, + params: [{ + eq: [ + { + field: 'parent', + value: parent + } + ], + sort: ['index ASC'], + limit: 100 + }] + }; + + return >( + returnResponse( + () => fetch(RpcEndpoint, generatePostReq(JSON.stringify(requestPayload))), + i18next.t("errorFetchingKeys") + ) + ); +}; diff --git a/ui/client/src/queries/rpcMethods.ts b/ui/client/src/queries/rpcMethods.ts index 35eafc9f0..d481b2d6a 100644 --- a/ui/client/src/queries/rpcMethods.ts +++ b/ui/client/src/queries/rpcMethods.ts @@ -33,5 +33,6 @@ export const RpcMethods = { reg_Registries: "reg_registries", transport_nodeName: "transport_nodeName", transport_localTransportDetails: "transport_localTransportDetails", - transport_peers: "transport_peers" + transport_peers: "transport_peers", + keymgr_queryKeys: "keymgr_queryKeys" }; diff --git a/ui/client/src/routes.ts b/ui/client/src/routes.ts index 7fd2502f5..a5c3952eb 100644 --- a/ui/client/src/routes.ts +++ b/ui/client/src/routes.ts @@ -18,5 +18,6 @@ export const AppRoutes = { Indexer: '/ui/indexer', Submissions: '/ui/submissions', Peers: '/ui/peers', + Keys: '/ui/keys', Registry: '/ui/registry' } \ No newline at end of file diff --git a/ui/client/src/translations/en.json b/ui/client/src/translations/en.json index 5559e47f8..8d124e95c 100644 --- a/ui/client/src/translations/en.json +++ b/ui/client/src/translations/en.json @@ -37,6 +37,7 @@ "epoch": "Epoch", "errorConnectingToPaladinNode": "Error connecting to Paladin node", "errorFetchingDomainReceipt": "Error fetching domain receipt", + "errorFetchingKeys": "Error fetching keys", "errorFetchingLatestBlock": "Error fetching latest block", "errorFetchingLatestEvents": "Error fetching latest events", "errorFetchingPaladinTransactions": "Error fetching Paladin transactions", @@ -59,15 +60,19 @@ "externalCallsEnabled": "External Calls Enabled", "from": "From", "group": "Group", + "handle": "Handle", "hash": "Hash", "hideProperties": "Hide Properties", "id": "ID", + "index": "Index", "indexer": "Indexer", "identityHash": "Identity Hash", + "keys": "Keys", "lastReceive": "Last Receive", "lastSend": "Last Send", "light": "Light", "live": "Live", + "localKeys": "Local Keys", "localTime": "Local Time", "logIndex": "Log Index", "messagesReceived": "Messages Received", @@ -100,6 +105,7 @@ "reliableHighestAck": "Reliable Highest Ack", "reliableHighestSent": "Reliable Highest Sent", "result": "Result", + "root": "Root", "showProperties": "Show Properties", "signature": "Signature", "stateReceipt": "State Receipt", @@ -124,5 +130,7 @@ "transportSubtitle": "{{transport}} {{detail}}", "type": "Type", "value": "Value", - "viewDetails": "View Details" + "verifier": "Verifier", + "viewDetails": "View Details", + "wallet": "Wallet" } \ No newline at end of file diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx new file mode 100644 index 000000000..06feba731 --- /dev/null +++ b/ui/client/src/views/Keys.tsx @@ -0,0 +1,142 @@ +// Copyright © 2025 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 { Alert, Box, Breadcrumbs, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, useTheme } from "@mui/material"; +import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; +import { useContext, useState } from "react"; +import { ApplicationContext } from "../contexts/ApplicationContext"; +import { fetchKeys } from "../queries/keys"; +import { Hash } from "../components/Hash"; +import AddBoxIcon from '@mui/icons-material/AddBox'; +import NavigateNextIcon from '@mui/icons-material/NavigateNext'; +import { getAltModeScrollBarStyle } from "../themes/default"; + +export const Keys: React.FC = () => { + + const { lastBlockWithTransactions, autoRefreshEnabled } = useContext(ApplicationContext); + const [parent, setParent] = useState(''); + const theme = useTheme(); + + const { data: keys, error, isFetching } = useQuery({ + queryKey: ["keys", autoRefreshEnabled, lastBlockWithTransactions, parent], + queryFn: () => fetchKeys(parent) + }); + + if (isFetching) { + return <>; + } + + if (error) { + return {error.message} + } + + + let breadcrumbContent: JSX.Element[] = []; + if (parent !== '') { + const segments = parent.split('.'); + let segmentStack: string[] = []; + for (const segment of segments) { + segmentStack.push(segment); + const target = segmentStack.join('.'); + breadcrumbContent.push( + { + event.preventDefault(); + setParent(target); + }}> + {segment === '' ? t('root') : segment} + + ) + } + } + + return ( + + + + {t("localKeys")} + + } + sx={{ marginLeft: '10px', marginBottom: '10px' }}> + { event.preventDefault(); setParent('') }}> + {t('root')} + + {breadcrumbContent} + + + + + + + + {t('name')} + {t('index')} + {t('type')} + {t('verifier')} + {t('wallet')} + {t('handle')} + + + + {keys?.map(key => + + {key.hasChildren && + setParent(key.path)}> + + + } + {key.path} + {key.index} + {key.type} + + {key.verifier && + } + + {key.wallet} + {key.keyHandle} + + )} + +
+
+
+
+
+ ); + +} From 6e0e523db610b1c93eaf205849b663d5b7368150 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 28 Jan 2025 16:04:37 -0500 Subject: [PATCH 02/10] UI update to reflect changes in API Signed-off-by: Gabriel Indik --- ui/client/src/components/Hash.tsx | 7 +- ui/client/src/components/Transactions.tsx | 2 +- ui/client/src/components/config.ts | 3 + ui/client/src/interfaces.ts | 6 +- ui/client/src/queries/keys.ts | 16 +- ui/client/src/themes/default.ts | 2 +- ui/client/src/translations/en.json | 3 + ui/client/src/views/Keys.tsx | 210 +++++++++++++++++----- 8 files changed, 191 insertions(+), 58 deletions(-) diff --git a/ui/client/src/components/Hash.tsx b/ui/client/src/components/Hash.tsx index 4aab37e57..a0045f6b5 100644 --- a/ui/client/src/components/Hash.tsx +++ b/ui/client/src/components/Hash.tsx @@ -25,9 +25,10 @@ type Props = { title: string hideTitle?: boolean hash: string + secondary?: boolean } -export const Hash: React.FC = ({ Icon, title, hideTitle, hash }) => { +export const Hash: React.FC = ({ Icon, title, hideTitle, hash, secondary }) => { const [hashDialogOpen, setHashDialogOpen] = useState(false); @@ -35,7 +36,7 @@ export const Hash: React.FC = ({ Icon, title, hideTitle, hash }) => { if (value.length < MAX_LENGTH_WITHOUT_COLLAPSE) { return hash; } - return `${value.substring(0, 5)}...${value.substring(value.length - 3)}` + return `${value.substring(0, 6)}...${value.substring(value.length - 4)}` }; const content = hideTitle ? getHash(hash) : `${title} | ${getHash(hash)}` @@ -48,7 +49,7 @@ export const Hash: React.FC = ({ Icon, title, hideTitle, hash }) => { onClick={() => setHashDialogOpen(true)} fullWidth variant="contained" - color="secondary" + color={ secondary? 'secondary' : 'primary' } sx={{ paddingTop: 0, paddingBottom: 0, textTransform: 'none', fontWeight: '400', whiteSpace: 'nowrap' }} size="small"> {content} diff --git a/ui/client/src/components/Transactions.tsx b/ui/client/src/components/Transactions.tsx index 2e8cc0a26..9dc13f315 100644 --- a/ui/client/src/components/Transactions.tsx +++ b/ui/client/src/components/Transactions.tsx @@ -68,7 +68,7 @@ export const Transactions: React.FC = () => { hasMore={hasNextPage} loader={} > - {transactions.pages.map(transactionArrat => transactionArrat?.map((transaction) => ( + {transactions.pages.map(transactionArray => transactionArray.map((transaction) => ( => { - const requestPayload = { +export const fetchKeys = async (parent: string, sortBy: string, sortOrder: 'asc' | 'desc', pageParam?: IKeyEntry): Promise => { + let requestPayload: any = { jsonrpc: "2.0", id: Date.now(), method: RpcMethods.keymgr_queryKeys, @@ -31,11 +32,18 @@ export const fetchKeys = async (parent: string): Promise => { value: parent } ], - sort: ['index ASC'], - limit: 100 + sort: [`${sortBy} ${sortOrder}`], + limit: constants.KEY_QUERY_LIMIT }] }; + if(pageParam !== undefined) { + requestPayload.params[0][sortOrder === 'asc' ? 'greaterThan' : 'lessThan'] = [{ + field: sortBy, + value: pageParam[sortBy as 'path' | 'index'] + }]; + } + return >( returnResponse( () => fetch(RpcEndpoint, generatePostReq(JSON.stringify(requestPayload))), diff --git a/ui/client/src/themes/default.ts b/ui/client/src/themes/default.ts index 3dcaaa1bb..3eb2f8ca2 100644 --- a/ui/client/src/themes/default.ts +++ b/ui/client/src/themes/default.ts @@ -25,7 +25,7 @@ export const darkThemeOptions: ThemeOptions = { dark: '#6D6D6D' }, secondary: { - main: '#20dfdf', + main: '#bbbbbb', }, background: { default: 'black', diff --git a/ui/client/src/translations/en.json b/ui/client/src/translations/en.json index 8d124e95c..dea75f6f2 100644 --- a/ui/client/src/translations/en.json +++ b/ui/client/src/translations/en.json @@ -4,6 +4,7 @@ "abiHash": "ABI Hash: {{hash}}", "activated": "Activated", "activePeers": "Active Peers", + "address": "Address", "implementation": "Implementation", "invalidABI": "Invalid ABI", "active": "Active", @@ -88,6 +89,8 @@ "numberSeconds": "{{number}} (seconds)", "off": "Off", "on": "On", + "openFolder": "Open folder", + "otherVerifiers": "Other Verifiers", "owner": "Owner", "paladin": "Paladin", "parentIdentityHash": "Parent Identity Hash", diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx index 06feba731..d8092fffe 100644 --- a/ui/client/src/views/Keys.tsx +++ b/ui/client/src/views/Keys.tsx @@ -14,36 +14,77 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Alert, Box, Breadcrumbs, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, useTheme } from "@mui/material"; -import { useQuery } from "@tanstack/react-query"; +import { Alert, Box, Breadcrumbs, Fade, IconButton, LinearProgress, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, Tooltip, Typography, useTheme } from "@mui/material"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { t } from "i18next"; -import { useContext, useState } from "react"; -import { ApplicationContext } from "../contexts/ApplicationContext"; +import { useEffect, useRef, useState } from "react"; import { fetchKeys } from "../queries/keys"; import { Hash } from "../components/Hash"; -import AddBoxIcon from '@mui/icons-material/AddBox'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import { getAltModeScrollBarStyle } from "../themes/default"; +import InfiniteScroll from "react-infinite-scroll-component"; +import { IKeyEntry } from "../interfaces"; +import { useSearchParams } from "react-router-dom"; +import { Captions, Signature } from "lucide-react"; +import { constants } from "../components/config"; export const Keys: React.FC = () => { - const { lastBlockWithTransactions, autoRefreshEnabled } = useContext(ApplicationContext); - const [parent, setParent] = useState(''); + const [searchParams, setSearchParams] = useSearchParams(); + const [parent, setParent] = useState(searchParams.get('path') ?? ''); + const [sortBy, setSortBy] = useState(window.localStorage.getItem(constants.KEY_SORT_BY_STORAGE_KEY) ?? 'index'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>( + window.localStorage.getItem(constants.KEY_SORT_ORDER_STORAGE_KEY) as 'asc' | 'desc' ?? 'asc'); + const scrollRef = useRef(); const theme = useTheme(); - const { data: keys, error, isFetching } = useQuery({ - queryKey: ["keys", autoRefreshEnabled, lastBlockWithTransactions, parent], - queryFn: () => fetchKeys(parent) + const { data: keys, fetchNextPage, hasNextPage, error } = useInfiniteQuery({ + queryKey: ["keys", parent, sortBy, sortOrder], + queryFn: ({ pageParam }) => fetchKeys(parent, sortBy, sortOrder, pageParam), + initialPageParam: undefined as IKeyEntry | undefined, + getNextPageParam: (lastPage) => { return lastPage[lastPage.length - 1] }, }); - if (isFetching) { - return <>; - } + useEffect(() => { + if (keys !== undefined + && hasNextPage + && scrollRef?.current !== undefined + && scrollRef.current.scrollHeight === scrollRef.current.clientHeight) { + fetchNextPage(); + } + }, [keys, hasNextPage, scrollRef]); + + useEffect(() => { + if (parent === '') { + setSearchParams({}); + } else { + setSearchParams({ path: parent }); + } + }, [parent]) if (error) { return {error.message} } + if (keys?.pages === undefined) { + return <>; + } + + const handleSortChange = (column: string) => { + if (column === sortBy) { + const order = sortOrder === 'asc' ? 'desc' : 'asc'; + setSortOrder(order); + window.localStorage.setItem(constants.KEY_SORT_ORDER_STORAGE_KEY, order); + } else { + window.localStorage.setItem(constants.KEY_SORT_BY_STORAGE_KEY, column); + if (sortOrder !== 'asc') { + window.localStorage.setItem(constants.KEY_SORT_ORDER_STORAGE_KEY, 'asc'); + setSortOrder('asc'); + } + setSortBy(column); + } + }; let breadcrumbContent: JSX.Element[] = []; if (parent !== '') { @@ -67,8 +108,47 @@ export const Keys: React.FC = () => { } } + const getEthAddress = (key: IKeyEntry) => { + const entry = key.verifiers?.find(entry => entry.type === 'eth_address'); + if (entry !== undefined) { + return } + title={entry.algorithm} + hash={entry.verifier} + hideTitle /> + } + return '--'; + }; + + const getOtherVerifiers = (key: IKeyEntry) => { + if (key.verifiers !== null) { + const entries = key.verifiers.filter(entry => entry.type !== 'eth_address'); + if (entries.length === 1) { + return } + title={entries[0].algorithm} + hash={entries[0].verifier} + hideTitle /> + } else if (entries.length > 1) { + // TODO: once we have more than 2, we should have an experience for listing + // an arbitrary number of verifiers + } + } + return '--'; + }; + + const headerDivider = `solid 1px ${theme.palette.divider}`, + position: 'absolute', + top: '14px', + left: '2px' + }} />; + + return ( - + { {breadcrumbContent} - - - - - - {t('name')} - {t('index')} - {t('type')} - {t('verifier')} - {t('wallet')} - {t('handle')} - - - - {keys?.map(key => - - {key.hasChildren && - setParent(key.path)}> - - - } - {key.path} - {key.index} - {key.type} + fetchNextPage()} + hasMore={hasNextPage} + loader={} + > + +
+ + - {key.verifier && - } - {key.wallet} - {key.keyHandle} + + handleSortChange('path')} + > + {t('name')} + + {headerDivider} + + + handleSortChange('index')} + > + {t('index')} + + {headerDivider} + + {t('address')}{headerDivider} + {t('otherVerifiers')}{headerDivider} + {t('wallet')}{headerDivider} + {t('handle')}{headerDivider} - )} - -
-
+ + + {keys.pages.map(keyArray => keyArray.map(key => + + {key.hasChildren && + + setParent(key.path)}> + + + + } + {key.path} + {key.index} + + {getEthAddress(key)} + + + {getOtherVerifiers(key)} + + {key.wallet.length > 0 ? key.wallet : '--'} + {key.keyHandle.length > 0 ? + + : '--'} + + ))} + + + +
From 451f3e3faeec9305d7d0f5674984e2fdf8cc4a20 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 28 Jan 2025 17:05:49 -0500 Subject: [PATCH 03/10] Pagination to key table Signed-off-by: Gabriel Indik --- ui/client/src/components/config.ts | 1 - ui/client/src/queries/keys.ts | 9 +- ui/client/src/views/Keys.tsx | 224 ++++++++++++++++------------- 3 files changed, 131 insertions(+), 103 deletions(-) diff --git a/ui/client/src/components/config.ts b/ui/client/src/components/config.ts index cad1c71d1..988810963 100644 --- a/ui/client/src/components/config.ts +++ b/ui/client/src/components/config.ts @@ -22,7 +22,6 @@ export const constants = { SUBMISSIONS_QUERY_LIMIT: 10, REGISTRY_ENTRIES_QUERY_LIMIT: 100, TRANSACTION_QUERY_LIMIT: 10, - KEY_QUERY_LIMIT: 10, UPDATE_FREQUENCY_MILLISECONDS: 5000, ELLAPSED_TIME_AUTO_REFRESH_FREQUENCY_SECONDS: 60 }; diff --git a/ui/client/src/queries/keys.ts b/ui/client/src/queries/keys.ts index 959dba16f..8edfabaaa 100644 --- a/ui/client/src/queries/keys.ts +++ b/ui/client/src/queries/keys.ts @@ -14,13 +14,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { constants } from "../components/config"; import { IKeyEntry } from "../interfaces"; import { generatePostReq, returnResponse } from "./common"; import { RpcEndpoint, RpcMethods } from "./rpcMethods"; import i18next from "i18next"; -export const fetchKeys = async (parent: string, sortBy: string, sortOrder: 'asc' | 'desc', pageParam?: IKeyEntry): Promise => { +export const fetchKeys = async (parent: string, limit: number, sortBy: string, sortOrder: 'asc' | 'desc', refEntry?: IKeyEntry): Promise => { let requestPayload: any = { jsonrpc: "2.0", id: Date.now(), @@ -33,14 +32,14 @@ export const fetchKeys = async (parent: string, sortBy: string, sortOrder: 'asc' } ], sort: [`${sortBy} ${sortOrder}`], - limit: constants.KEY_QUERY_LIMIT + limit }] }; - if(pageParam !== undefined) { + if(refEntry !== undefined) { requestPayload.params[0][sortOrder === 'asc' ? 'greaterThan' : 'lessThan'] = [{ field: sortBy, - value: pageParam[sortBy as 'path' | 'index'] + value: refEntry[sortBy as 'path' | 'index'] }]; } diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx index d8092fffe..a1e284a49 100644 --- a/ui/client/src/views/Keys.tsx +++ b/ui/client/src/views/Keys.tsx @@ -14,16 +14,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Alert, Box, Breadcrumbs, Fade, IconButton, LinearProgress, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, Tooltip, Typography, useTheme } from "@mui/material"; -import { useInfiniteQuery } from "@tanstack/react-query"; +import { Alert, Box, Breadcrumbs, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Tooltip, Typography } from "@mui/material"; +import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { fetchKeys } from "../queries/keys"; import { Hash } from "../components/Hash"; import FolderOpenIcon from '@mui/icons-material/FolderOpen'; import NavigateNextIcon from '@mui/icons-material/NavigateNext'; -import { getAltModeScrollBarStyle } from "../themes/default"; -import InfiniteScroll from "react-infinite-scroll-component"; import { IKeyEntry } from "../interfaces"; import { useSearchParams } from "react-router-dom"; import { Captions, Signature } from "lucide-react"; @@ -32,28 +30,39 @@ import { constants } from "../components/config"; export const Keys: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); + const [refEntries, setRefEntries] = useState([]); + const [page, setPage] = useState(0); + const [count, setCount] = useState(-1); + const [rowsPerPage, setRowsPerPage] = useState(10); const [parent, setParent] = useState(searchParams.get('path') ?? ''); const [sortBy, setSortBy] = useState(window.localStorage.getItem(constants.KEY_SORT_BY_STORAGE_KEY) ?? 'index'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>( window.localStorage.getItem(constants.KEY_SORT_ORDER_STORAGE_KEY) as 'asc' | 'desc' ?? 'asc'); - const scrollRef = useRef(); - const theme = useTheme(); - const { data: keys, fetchNextPage, hasNextPage, error } = useInfiniteQuery({ - queryKey: ["keys", parent, sortBy, sortOrder], - queryFn: ({ pageParam }) => fetchKeys(parent, sortBy, sortOrder, pageParam), - initialPageParam: undefined as IKeyEntry | undefined, - getNextPageParam: (lastPage) => { return lastPage[lastPage.length - 1] }, + const { data: keys, error } = useQuery({ + queryKey: ["keys", parent, sortBy, sortOrder, refEntries, rowsPerPage], + queryFn: () => fetchKeys(parent, rowsPerPage, sortBy, sortOrder, refEntries[refEntries.length - 1]) }); useEffect(() => { - if (keys !== undefined - && hasNextPage - && scrollRef?.current !== undefined - && scrollRef.current.scrollHeight === scrollRef.current.clientHeight) { - fetchNextPage(); + if (count !== -1 && (page * rowsPerPage === count)) { + handleChangePage(null, page - 1); } - }, [keys, hasNextPage, scrollRef]); + }, [count, rowsPerPage, page]); + + useEffect(() => { + if (keys !== undefined && count === -1) { + if (keys.length < rowsPerPage) { + setCount(rowsPerPage * page + keys.length); + } + } + }, [keys, rowsPerPage, page]); + + useEffect(() => { + setCount(-1); + setPage(0); + setRefEntries([]); + }, [parent]); useEffect(() => { if (parent === '') { @@ -61,13 +70,13 @@ export const Keys: React.FC = () => { } else { setSearchParams({ path: parent }); } - }, [parent]) + }, [parent, page]); if (error) { return {error.message} } - if (keys?.pages === undefined) { + if (keys === undefined) { return <>; } @@ -137,6 +146,27 @@ export const Keys: React.FC = () => { return '--'; }; + const handleChangePage = ( + _event: React.MouseEvent | null, + newPage: number + ) => { + if (newPage === 0) { + setRefEntries([]); + } else if (newPage > page) { + refEntries.push(keys[keys.length - 1]); + } else { + refEntries.pop(); + } + setPage(newPage); + }; + + const handleChangeRowsPerPage = ( + event: React.ChangeEvent + ) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + const headerDivider = { {breadcrumbContent} - - fetchNextPage()} - hasMore={hasNextPage} - loader={} - > - - - - - - - - handleSortChange('path')} - > - {t('name')} - - {headerDivider} - - - handleSortChange('index')} - > - {t('index')} - - {headerDivider} - - {t('address')}{headerDivider} - {t('otherVerifiers')}{headerDivider} - {t('wallet')}{headerDivider} - {t('handle')}{headerDivider} - - - - {keys.pages.map(keyArray => keyArray.map(key => - - {key.hasChildren && - - setParent(key.path)}> - - - - } - {key.path} - {key.index} - - {getEthAddress(key)} - - - {getOtherVerifiers(key)} - - {key.wallet.length > 0 ? key.wallet : '--'} - {key.keyHandle.length > 0 ? - - : '--'} - - ))} - -
-
-
-
+ + + + + + + + + handleSortChange('path')} + > + {t('name')} + + {headerDivider} + + + handleSortChange('index')} + > + {t('index')} + + {headerDivider} + + {t('address')}{headerDivider} + {t('otherVerifiers')}{headerDivider} + {t('wallet')}{headerDivider} + {t('handle')}{headerDivider} + + + + {keys.map(key => + + {key.hasChildren && + + setParent(key.path)}> + + + + } + {key.path} + {key.index} + + {getEthAddress(key)} + + + {getOtherVerifiers(key)} + + {key.wallet.length > 0 ? key.wallet : '--'} + {key.keyHandle.length > 0 ? + + : '--'} + + )} + +
+ +
); From 6e971faf4fa563853928aa9eb3cb2e9db58c4c05 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Tue, 28 Jan 2025 17:18:19 -0500 Subject: [PATCH 04/10] reset references on rows per page change Signed-off-by: Gabriel Indik --- ui/client/src/views/Keys.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx index a1e284a49..547263a34 100644 --- a/ui/client/src/views/Keys.tsx +++ b/ui/client/src/views/Keys.tsx @@ -164,6 +164,7 @@ export const Keys: React.FC = () => { event: React.ChangeEvent ) => { setRowsPerPage(parseInt(event.target.value, 10)); + setRefEntries([]); setPage(0); }; From fe507b6a3f18589236d27180f0ec4b6dd03d9b96 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Wed, 29 Jan 2025 16:17:33 -0500 Subject: [PATCH 05/10] Reverse lookup Signed-off-by: Gabriel Indik --- ui/client/src/components/config.ts | 5 +- ui/client/src/dialogs/ReverseKeyLookup.tsx | 189 ++++++++++++++ ui/client/src/interfaces.ts | 15 ++ ui/client/src/queries/keys.ts | 27 +- ui/client/src/queries/rpcMethods.ts | 3 +- ui/client/src/translations/en.json | 7 + ui/client/src/views/Keys.tsx | 283 ++++++++++++--------- ui/client/src/views/Peers.tsx | 4 +- 8 files changed, 410 insertions(+), 123 deletions(-) create mode 100644 ui/client/src/dialogs/ReverseKeyLookup.tsx diff --git a/ui/client/src/components/config.ts b/ui/client/src/components/config.ts index 988810963..5ea712209 100644 --- a/ui/client/src/components/config.ts +++ b/ui/client/src/components/config.ts @@ -16,8 +16,9 @@ export const constants = { COLOR_MODE_STORAGE_KEY: 'color-mode', - KEY_SORT_BY_STORAGE_KEY: 'sort-by', - KEY_SORT_ORDER_STORAGE_KEY: 'sort-order', + KEYS_SORT_BY_STORAGE_KEY: 'keys-sort-by', + KEYS_SORT_ORDER_STORAGE_KEY: 'keys-sort-order', + KEYS_ROWS_PER_PAGE: 'keys-rows-per-page', EVENT_QUERY_LIMIT: 10, SUBMISSIONS_QUERY_LIMIT: 10, REGISTRY_ENTRIES_QUERY_LIMIT: 100, diff --git a/ui/client/src/dialogs/ReverseKeyLookup.tsx b/ui/client/src/dialogs/ReverseKeyLookup.tsx new file mode 100644 index 000000000..749b79467 --- /dev/null +++ b/ui/client/src/dialogs/ReverseKeyLookup.tsx @@ -0,0 +1,189 @@ +// Copyright © 2024 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 { + Alert, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Radio, + RadioGroup, + TextField +} from '@mui/material'; +import { useQuery } from '@tanstack/react-query'; +import { t } from 'i18next'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { reverseKeyLookup } from '../queries/keys'; + +type Props = { + dialogOpen: boolean + setDialogOpen: React.Dispatch> + setParent: Dispatch> + setPathFilter: Dispatch> +} + +export const ReverseKeyLookupDialog: React.FC = ({ + dialogOpen, + setDialogOpen, + setParent, + setPathFilter +}) => { + + const [verifier, setVerifier] = useState(''); + const [isEthereum, setIsEthereum] = useState(true); + const [type, setType] = useState(''); + const [otherType, setOtherType] = useState(''); + const [otherAlgorithm, setOtherAlgorithm] = useState(''); + const [algorithm, setAlgorithm] = useState(''); + const [notFound, setNotFound] = useState(false); + + const { refetch } = useQuery({ + queryKey: ["reverseKeyLookup"], + queryFn: () => reverseKeyLookup(algorithm, type, verifier), + enabled: false, + refetchOnMount: false, + retry: false + }); + + useEffect(() => { + if (!dialogOpen) { + setTimeout(() => { + setVerifier(''); + setIsEthereum(true); + setOtherType(''); + setOtherAlgorithm(''); + setNotFound(false); + }, 200); + } + }, [dialogOpen]); + + useEffect(() => { + setType(isEthereum ? 'eth_address' : otherType); + setAlgorithm(isEthereum ? 'ecdsa:secp256k1' : otherAlgorithm); + }, [isEthereum, otherType, otherAlgorithm]); + + const handleSubmit = () => { + refetch().then(result => { + if (result.status === 'success') { + const path = result.data.path.map(segment => segment.name).join('.'); + if (path.includes('.')) { + setParent(path.substring(0, path.lastIndexOf('.'))) + } + setPathFilter(path); + setDialogOpen(false); + } else if (result.status === 'error') { + setNotFound(true); + } + }); + }; + + const canSubmit = verifier.length > 0 && (isEthereum || + (otherType.length > 0 && otherAlgorithm.length > 0)); + + return ( + setDialogOpen(false)} + > +
{ + event.preventDefault(); + handleSubmit(); + }}> + + {t('reverseLookup')} + {notFound && + {t('verifierNotFound')}} + + + + setVerifier(event.target.value)} /> + + setIsEthereum(event.target.value === 'ethereum')} + > + } label={t('ethereum')} /> + } label={t('other')} /> + + + setOtherType(event.target.value)}> + + setOtherAlgorithm(event.target.value)}> + + + + + + + + + +
+
+ ); +}; diff --git a/ui/client/src/interfaces.ts b/ui/client/src/interfaces.ts index 61516ac07..3a6668bd6 100644 --- a/ui/client/src/interfaces.ts +++ b/ui/client/src/interfaces.ts @@ -160,3 +160,18 @@ export interface IKeyEntry { wallet: string keyHandle: string } + +export interface IKeyMappingAndVerifier { + identifier: string + keyHandle: string + path: { + index: number + name: string + }[] + verifier: { + verifier: string + type: string + algorithm: string + }, + wallet: string +} \ No newline at end of file diff --git a/ui/client/src/queries/keys.ts b/ui/client/src/queries/keys.ts index 8edfabaaa..ddcae5d85 100644 --- a/ui/client/src/queries/keys.ts +++ b/ui/client/src/queries/keys.ts @@ -14,12 +14,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { IKeyEntry } from "../interfaces"; +import { IKeyEntry, IKeyMappingAndVerifier } from "../interfaces"; import { generatePostReq, returnResponse } from "./common"; import { RpcEndpoint, RpcMethods } from "./rpcMethods"; import i18next from "i18next"; -export const fetchKeys = async (parent: string, limit: number, sortBy: string, sortOrder: 'asc' | 'desc', refEntry?: IKeyEntry): Promise => { +export const fetchKeys = async (parent: string, limit: number, sortBy: string, sortOrder: 'asc' | 'desc', pathFilter?: string, refEntry?: IKeyEntry): Promise => { let requestPayload: any = { jsonrpc: "2.0", id: Date.now(), @@ -43,6 +43,13 @@ export const fetchKeys = async (parent: string, limit: number, sortBy: string, s }]; } + if(pathFilter !== undefined) { + requestPayload.params[0].eq.push({ + field: 'path', + value: pathFilter + }); + } + return >( returnResponse( () => fetch(RpcEndpoint, generatePostReq(JSON.stringify(requestPayload))), @@ -50,3 +57,19 @@ export const fetchKeys = async (parent: string, limit: number, sortBy: string, s ) ); }; + +export const reverseKeyLookup = async (algorithm: string, verifierType: string, verifier: string): Promise => { + const requestPayload = { + jsonrpc: "2.0", + id: Date.now(), + method: RpcMethods.keymgr_reverseKeyLookup, + params: [algorithm, verifierType, verifier] + }; + + return >( + returnResponse( + () => fetch(RpcEndpoint, generatePostReq(JSON.stringify(requestPayload))), + i18next.t("errorFetchingReverseKeyLookup"), [] + ) + ); +}; diff --git a/ui/client/src/queries/rpcMethods.ts b/ui/client/src/queries/rpcMethods.ts index d481b2d6a..f6820dec3 100644 --- a/ui/client/src/queries/rpcMethods.ts +++ b/ui/client/src/queries/rpcMethods.ts @@ -34,5 +34,6 @@ export const RpcMethods = { transport_nodeName: "transport_nodeName", transport_localTransportDetails: "transport_localTransportDetails", transport_peers: "transport_peers", - keymgr_queryKeys: "keymgr_queryKeys" + keymgr_queryKeys: "keymgr_queryKeys", + keymgr_reverseKeyLookup: "keymgr_reverseKeyLookup" }; diff --git a/ui/client/src/translations/en.json b/ui/client/src/translations/en.json index dea75f6f2..a161d0301 100644 --- a/ui/client/src/translations/en.json +++ b/ui/client/src/translations/en.json @@ -5,6 +5,7 @@ "activated": "Activated", "activePeers": "Active Peers", "address": "Address", + "algorithm": "Algorithm", "implementation": "Implementation", "invalidABI": "Invalid ABI", "active": "Active", @@ -52,6 +53,8 @@ "errorFetchingTransportLocalDetails": "Error fetching transport local details", "errorFetchingTransportNodeName": "Error fetching transport node name", "errorFetchingTransportPeers": "Error fetching transport peers", + "errorFetchingReverseKeyLookup": "Error fetching reverse key lookup", + "ethereum": "Ethereum", "event": "Event", "events": "Events", "evmPrivateLog": "EVM Private Log {{logIndex}}", @@ -76,6 +79,7 @@ "localKeys": "Local Keys", "localTime": "Local Time", "logIndex": "Log Index", + "lookup": "Lookup", "messagesReceived": "Messages Received", "messagesSent": "Messages Sent", "name": "Name", @@ -90,6 +94,7 @@ "off": "Off", "on": "On", "openFolder": "Open folder", + "other": "Other", "otherVerifiers": "Other Verifiers", "owner": "Owner", "paladin": "Paladin", @@ -108,6 +113,7 @@ "reliableHighestAck": "Reliable Highest Ack", "reliableHighestSent": "Reliable Highest Sent", "result": "Result", + "reverseLookup": "Reverse Lookup", "root": "Root", "showProperties": "Show Properties", "signature": "Signature", @@ -134,6 +140,7 @@ "type": "Type", "value": "Value", "verifier": "Verifier", + "verifierNotFound": "Verifier not found", "viewDetails": "View Details", "wallet": "Wallet" } \ No newline at end of file diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx index 547263a34..9bc391923 100644 --- a/ui/client/src/views/Keys.tsx +++ b/ui/client/src/views/Keys.tsx @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Alert, Box, Breadcrumbs, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Tooltip, Typography } from "@mui/material"; +import { Alert, Box, Breadcrumbs, Button, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Tooltip, Typography } from "@mui/material"; import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { useEffect, useState } from "react"; @@ -26,22 +26,42 @@ import { IKeyEntry } from "../interfaces"; import { useSearchParams } from "react-router-dom"; import { Captions, Signature } from "lucide-react"; import { constants } from "../components/config"; +import SearchIcon from '@mui/icons-material/Search'; +import { ReverseKeyLookupDialog } from "../dialogs/ReverseKeyLookup"; +import RemoveIcon from '@mui/icons-material/Remove'; export const Keys: React.FC = () => { + const getDefaultRowsPerPage = () => { + const valueFromStorage = window.localStorage.getItem(constants.KEYS_ROWS_PER_PAGE); + if (valueFromStorage !== null) { + return Number(valueFromStorage); + } + return 10; + }; + + const getDefaultSortBy = () => { + return window.localStorage.getItem(constants.KEYS_SORT_BY_STORAGE_KEY) ?? 'index'; + }; + + const getDefaultSortOrder = () => { + return window.localStorage.getItem(constants.KEYS_SORT_ORDER_STORAGE_KEY) as 'asc' | 'desc' ?? 'asc'; + }; + const [searchParams, setSearchParams] = useSearchParams(); const [refEntries, setRefEntries] = useState([]); + const [pathFilter, setPathFilter] = useState(); const [page, setPage] = useState(0); const [count, setCount] = useState(-1); - const [rowsPerPage, setRowsPerPage] = useState(10); + const [rowsPerPage, setRowsPerPage] = useState(getDefaultRowsPerPage()); const [parent, setParent] = useState(searchParams.get('path') ?? ''); - const [sortBy, setSortBy] = useState(window.localStorage.getItem(constants.KEY_SORT_BY_STORAGE_KEY) ?? 'index'); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>( - window.localStorage.getItem(constants.KEY_SORT_ORDER_STORAGE_KEY) as 'asc' | 'desc' ?? 'asc'); + const [reverseLookupDialogOpen, setReverseLookupDialogOpen] = useState(false); + const [sortBy, setSortBy] = useState(getDefaultSortBy()); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(getDefaultSortOrder); const { data: keys, error } = useQuery({ - queryKey: ["keys", parent, sortBy, sortOrder, refEntries, rowsPerPage], - queryFn: () => fetchKeys(parent, rowsPerPage, sortBy, sortOrder, refEntries[refEntries.length - 1]) + queryKey: ["keys", parent, sortBy, sortOrder, refEntries, rowsPerPage, pathFilter], + queryFn: () => fetchKeys(parent, rowsPerPage, sortBy, sortOrder, pathFilter, refEntries[refEntries.length - 1]) }); useEffect(() => { @@ -80,15 +100,23 @@ export const Keys: React.FC = () => { return <>; } + const removeParentFromPath = (path: string) => { + let index = parent.length; + if (index > 0) { + index++; + } + return path.substring(index); + } + const handleSortChange = (column: string) => { if (column === sortBy) { const order = sortOrder === 'asc' ? 'desc' : 'asc'; setSortOrder(order); - window.localStorage.setItem(constants.KEY_SORT_ORDER_STORAGE_KEY, order); + window.localStorage.setItem(constants.KEYS_SORT_ORDER_STORAGE_KEY, order); } else { - window.localStorage.setItem(constants.KEY_SORT_BY_STORAGE_KEY, column); + window.localStorage.setItem(constants.KEYS_SORT_BY_STORAGE_KEY, column); if (sortOrder !== 'asc') { - window.localStorage.setItem(constants.KEY_SORT_ORDER_STORAGE_KEY, 'asc'); + window.localStorage.setItem(constants.KEYS_SORT_ORDER_STORAGE_KEY, 'asc'); setSortOrder('asc'); } setSortBy(column); @@ -105,10 +133,11 @@ export const Keys: React.FC = () => { breadcrumbContent.push( { event.preventDefault(); + setPathFilter(undefined); setParent(target); }}> {segment === '' ? t('root') : segment} @@ -116,6 +145,14 @@ export const Keys: React.FC = () => { ) } } + if (pathFilter !== undefined) { + breadcrumbContent.push( + + {removeParentFromPath(pathFilter)} + ); + } const getEthAddress = (key: IKeyEntry) => { const entry = key.verifiers?.find(entry => entry.type === 'eth_address'); @@ -126,7 +163,7 @@ export const Keys: React.FC = () => { hash={entry.verifier} hideTitle /> } - return '--'; + return ; }; const getOtherVerifiers = (key: IKeyEntry) => { @@ -139,11 +176,10 @@ export const Keys: React.FC = () => { hash={entries[0].verifier} hideTitle /> } else if (entries.length > 1) { - // TODO: once we have more than 2, we should have an experience for listing - // an arbitrary number of verifiers + // TODO: expand once there are more than 2 } } - return '--'; + return ; }; const handleChangePage = ( @@ -163,7 +199,9 @@ export const Keys: React.FC = () => { const handleChangeRowsPerPage = ( event: React.ChangeEvent ) => { - setRowsPerPage(parseInt(event.target.value, 10)); + const value = parseInt(event.target.value, 10); + setRowsPerPage(value); + window.localStorage.setItem(constants.KEYS_ROWS_PER_PAGE, value.toString()); setRefEntries([]); setPage(0); }; @@ -179,109 +217,124 @@ export const Keys: React.FC = () => { return ( - - - - {t("localKeys")} - - } - sx={{ marginLeft: '10px', marginBottom: '10px' }}> - { event.preventDefault(); setParent('') }}> - {t('root')} - - {breadcrumbContent} - + <> + + + + {t("localKeys")} + + + } + sx={{ marginLeft: '10px', marginBottom: '10px' }}> + { event.preventDefault(); setPathFilter(undefined); setParent('') }}> + {t('root')} + + {breadcrumbContent} + - - - - - - - - handleSortChange('path')} - > - {t('name')} - - {headerDivider} - - - handleSortChange('index')} - > - {t('index')} - - {headerDivider} - - {t('address')}{headerDivider} - {t('otherVerifiers')}{headerDivider} - {t('wallet')}{headerDivider} - {t('handle')}{headerDivider} - - - - {keys.map(key => - - {key.hasChildren && - - setParent(key.path)}> - - - - } - {key.path} - {key.index} - - {getEthAddress(key)} + +
+ + + + + handleSortChange('path')} + > + {t('name')} + + {headerDivider} - - {getOtherVerifiers(key)} + + handleSortChange('index')} + > + {t('index')} + + {headerDivider} - {key.wallet.length > 0 ? key.wallet : '--'} - {key.keyHandle.length > 0 ? - - : '--'} + {t('address')}{headerDivider} + {t('otherVerifiers')}{headerDivider} + {t('wallet')}{headerDivider} + {t('handle')}{headerDivider} - )} - -
- + + {keys.map(key => + + {key.hasChildren && + + { setPathFilter(undefined); setParent(key.path) }}> + + + + } + {removeParentFromPath(key.path)} + {key.index} + + {getEthAddress(key)} + + + {getOtherVerifiers(key)} + + {key.wallet.length > 0 ? key.wallet : } + {key.keyHandle.length > 0 ? + + : } + + )} + + + -
-
-
+ }} + component="div" + showFirstButton + showLastButton + count={count} + page={page} + onPageChange={handleChangePage} + rowsPerPage={rowsPerPage} + onRowsPerPageChange={handleChangeRowsPerPage} + /> + +
+
+ + ); - } diff --git a/ui/client/src/views/Peers.tsx b/ui/client/src/views/Peers.tsx index 6855254c4..5277ab061 100644 --- a/ui/client/src/views/Peers.tsx +++ b/ui/client/src/views/Peers.tsx @@ -76,9 +76,7 @@ export const Nodes: React.FC = () => { sx={{ position: 'absolute', right: '46px', top: '23px', textTransform: 'none', borderRadius: '20px' }} onClick={() => setMyNodeDialogOpen(true)} > - - {transportNodeName} - + {transportNodeName} Date: Wed, 29 Jan 2025 16:59:59 -0500 Subject: [PATCH 06/10] Multiple other verifiers dialog Signed-off-by: Gabriel Indik --- ui/client/src/dialogs/Verifiers.tsx | 74 +++++++++++++++++++++++++++++ ui/client/src/interfaces.ts | 18 ++++--- ui/client/src/translations/en.json | 2 + ui/client/src/views/Keys.tsx | 26 ++++++++-- 4 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 ui/client/src/dialogs/Verifiers.tsx diff --git a/ui/client/src/dialogs/Verifiers.tsx b/ui/client/src/dialogs/Verifiers.tsx new file mode 100644 index 000000000..6814a5b18 --- /dev/null +++ b/ui/client/src/dialogs/Verifiers.tsx @@ -0,0 +1,74 @@ +// Copyright © 2025 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 { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { IVerifier } from '../interfaces'; +import { SingleValue } from '../components/SingleValue'; + +type Props = { + dialogOpen: boolean + setDialogOpen: React.Dispatch> + verifiers: IVerifier[] +} + +export const VerifiersDialog: React.FC = ({ + dialogOpen, + setDialogOpen, + verifiers +}) => { + + const { t } = useTranslation(); + + return ( + setDialogOpen(false)} + maxWidth="md" + > + + {t('verifiers')} + + + + {verifiers.map(verifier => + + + + + )} + + + + + + + ); +}; diff --git a/ui/client/src/interfaces.ts b/ui/client/src/interfaces.ts index 3a6668bd6..731712fe8 100644 --- a/ui/client/src/interfaces.ts +++ b/ui/client/src/interfaces.ts @@ -146,17 +146,19 @@ export interface ITransportPeer { } } +export interface IVerifier { + verifier: string + type: string + algorithm: string +} + export interface IKeyEntry { isKey: boolean hasChildren: boolean path: string index: number type: string - verifiers: { - verifier: string - type: string - algorithm: string - }[] | null + verifiers: IVerifier[] | null wallet: string keyHandle: string } @@ -168,10 +170,6 @@ export interface IKeyMappingAndVerifier { index: number name: string }[] - verifier: { - verifier: string - type: string - algorithm: string - }, + verifier: IVerifier wallet: string } \ No newline at end of file diff --git a/ui/client/src/translations/en.json b/ui/client/src/translations/en.json index a161d0301..9daa7a80e 100644 --- a/ui/client/src/translations/en.json +++ b/ui/client/src/translations/en.json @@ -80,6 +80,7 @@ "localTime": "Local Time", "logIndex": "Log Index", "lookup": "Lookup", + "manyN": "Many ({{n}})", "messagesReceived": "Messages Received", "messagesSent": "Messages Sent", "name": "Name", @@ -140,6 +141,7 @@ "type": "Type", "value": "Value", "verifier": "Verifier", + "verifiers": "Verifiers", "verifierNotFound": "Verifier not found", "viewDetails": "View Details", "wallet": "Wallet" diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx index 9bc391923..27f528432 100644 --- a/ui/client/src/views/Keys.tsx +++ b/ui/client/src/views/Keys.tsx @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Alert, Box, Breadcrumbs, Button, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Tooltip, Typography } from "@mui/material"; +import { Alert, Box, Breadcrumbs, Button, Chip, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Tooltip, Typography } from "@mui/material"; import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { useEffect, useState } from "react"; @@ -22,13 +22,14 @@ import { fetchKeys } from "../queries/keys"; import { Hash } from "../components/Hash"; import FolderOpenIcon from '@mui/icons-material/FolderOpen'; import NavigateNextIcon from '@mui/icons-material/NavigateNext'; -import { IKeyEntry } from "../interfaces"; +import { IKeyEntry, IVerifier } from "../interfaces"; import { useSearchParams } from "react-router-dom"; import { Captions, Signature } from "lucide-react"; import { constants } from "../components/config"; import SearchIcon from '@mui/icons-material/Search'; import { ReverseKeyLookupDialog } from "../dialogs/ReverseKeyLookup"; import RemoveIcon from '@mui/icons-material/Remove'; +import { VerifiersDialog } from "../dialogs/Verifiers"; export const Keys: React.FC = () => { @@ -58,6 +59,8 @@ export const Keys: React.FC = () => { const [reverseLookupDialogOpen, setReverseLookupDialogOpen] = useState(false); const [sortBy, setSortBy] = useState(getDefaultSortBy()); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(getDefaultSortOrder); + const [selectedVerifiers, setSelectedVerifiers] = useState(); + const [verifiersDialogOpen, setVerifiersDialogOpen] = useState(false); const { data: keys, error } = useQuery({ queryKey: ["keys", parent, sortBy, sortOrder, refEntries, rowsPerPage, pathFilter], @@ -176,7 +179,18 @@ export const Keys: React.FC = () => { hash={entries[0].verifier} hideTitle /> } else if (entries.length > 1) { - // TODO: expand once there are more than 2 + return ( + + ); } } return ; @@ -335,6 +349,12 @@ export const Keys: React.FC = () => { setParent={setParent} setPathFilter={setPathFilter} /> + {selectedVerifiers && + } ); } From 86aee401499c9afa3245d61408d664da3cd8c895 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Thu, 30 Jan 2025 10:31:17 -0500 Subject: [PATCH 07/10] Query string improvements Signed-off-by: Gabriel Indik --- ui/client/src/dialogs/ReverseKeyLookup.tsx | 15 ++++++---- ui/client/src/queries/keys.ts | 6 ++-- ui/client/src/views/Keys.tsx | 35 ++++++++++++---------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/ui/client/src/dialogs/ReverseKeyLookup.tsx b/ui/client/src/dialogs/ReverseKeyLookup.tsx index 749b79467..e0b1f5734 100644 --- a/ui/client/src/dialogs/ReverseKeyLookup.tsx +++ b/ui/client/src/dialogs/ReverseKeyLookup.tsx @@ -1,4 +1,4 @@ -// Copyright © 2024 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -36,14 +36,14 @@ type Props = { dialogOpen: boolean setDialogOpen: React.Dispatch> setParent: Dispatch> - setPathFilter: Dispatch> + setFilter: Dispatch> } export const ReverseKeyLookupDialog: React.FC = ({ dialogOpen, setDialogOpen, setParent, - setPathFilter + setFilter }) => { const [verifier, setVerifier] = useState(''); @@ -83,10 +83,13 @@ export const ReverseKeyLookupDialog: React.FC = ({ refetch().then(result => { if (result.status === 'success') { const path = result.data.path.map(segment => segment.name).join('.'); - if (path.includes('.')) { - setParent(path.substring(0, path.lastIndexOf('.'))) + const index = path.lastIndexOf('.'); + if(index !== -1) { + setParent(path.substring(0, index)); + setFilter(path.substring(index + 1)) + } else { + setFilter(path); } - setPathFilter(path); setDialogOpen(false); } else if (result.status === 'error') { setNotFound(true); diff --git a/ui/client/src/queries/keys.ts b/ui/client/src/queries/keys.ts index ddcae5d85..519b61ac4 100644 --- a/ui/client/src/queries/keys.ts +++ b/ui/client/src/queries/keys.ts @@ -36,17 +36,17 @@ export const fetchKeys = async (parent: string, limit: number, sortBy: string, s }] }; - if(refEntry !== undefined) { + if (refEntry !== undefined) { requestPayload.params[0][sortOrder === 'asc' ? 'greaterThan' : 'lessThan'] = [{ field: sortBy, value: refEntry[sortBy as 'path' | 'index'] }]; } - if(pathFilter !== undefined) { + if (pathFilter !== undefined) { requestPayload.params[0].eq.push({ field: 'path', - value: pathFilter + value: parent !== '' ? `${parent}.${pathFilter}` : pathFilter }); } diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx index 27f528432..461df4428 100644 --- a/ui/client/src/views/Keys.tsx +++ b/ui/client/src/views/Keys.tsx @@ -14,7 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Alert, Box, Breadcrumbs, Button, Chip, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Tooltip, Typography } from "@mui/material"; +import { Alert, Box, Breadcrumbs, Button, Fade, IconButton, Link, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TableSortLabel, Tooltip, Typography } from "@mui/material"; import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { useEffect, useState } from "react"; @@ -51,7 +51,7 @@ export const Keys: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); const [refEntries, setRefEntries] = useState([]); - const [pathFilter, setPathFilter] = useState(); + const [filter, setFilter] = useState(searchParams.get('filter') ?? undefined); const [page, setPage] = useState(0); const [count, setCount] = useState(-1); const [rowsPerPage, setRowsPerPage] = useState(getDefaultRowsPerPage()); @@ -63,8 +63,8 @@ export const Keys: React.FC = () => { const [verifiersDialogOpen, setVerifiersDialogOpen] = useState(false); const { data: keys, error } = useQuery({ - queryKey: ["keys", parent, sortBy, sortOrder, refEntries, rowsPerPage, pathFilter], - queryFn: () => fetchKeys(parent, rowsPerPage, sortBy, sortOrder, pathFilter, refEntries[refEntries.length - 1]) + queryKey: ["keys", parent, sortBy, sortOrder, refEntries, rowsPerPage, filter], + queryFn: () => fetchKeys(parent, rowsPerPage, sortBy, sortOrder, filter, refEntries[refEntries.length - 1]) }); useEffect(() => { @@ -88,12 +88,15 @@ export const Keys: React.FC = () => { }, [parent]); useEffect(() => { - if (parent === '') { - setSearchParams({}); - } else { - setSearchParams({ path: parent }); + let value: any = {}; + if (parent !== '') { + value.path = parent; + } + if (filter !== undefined) { + value.filter = filter; } - }, [parent, page]); + setSearchParams(value); + }, [parent, page, filter]); if (error) { return {error.message} @@ -140,7 +143,7 @@ export const Keys: React.FC = () => { sx={{ textTransform: 'none' }} onClick={event => { event.preventDefault(); - setPathFilter(undefined); + setFilter(undefined); setParent(target); }}> {segment === '' ? t('root') : segment} @@ -148,12 +151,12 @@ export const Keys: React.FC = () => { ) } } - if (pathFilter !== undefined) { + if (filter !== undefined) { breadcrumbContent.push( - {removeParentFromPath(pathFilter)} + {filter} ); } @@ -260,7 +263,7 @@ export const Keys: React.FC = () => { { event.preventDefault(); setPathFilter(undefined); setParent('') }}> + onClick={event => { event.preventDefault(); setFilter(undefined); setParent('') }}> {t('root')} {breadcrumbContent} @@ -302,7 +305,7 @@ export const Keys: React.FC = () => { {key.hasChildren && - { setPathFilter(undefined); setParent(key.path) }}> + { setFilter(undefined); setParent(key.path) }}> @@ -347,7 +350,7 @@ export const Keys: React.FC = () => { dialogOpen={reverseLookupDialogOpen} setDialogOpen={setReverseLookupDialogOpen} setParent={setParent} - setPathFilter={setPathFilter} + setFilter={setFilter} /> {selectedVerifiers && Date: Thu, 30 Jan 2025 11:52:19 -0500 Subject: [PATCH 08/10] React to query string changes Signed-off-by: Gabriel Indik --- ui/client/src/views/Keys.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/client/src/views/Keys.tsx b/ui/client/src/views/Keys.tsx index 461df4428..b987d072a 100644 --- a/ui/client/src/views/Keys.tsx +++ b/ui/client/src/views/Keys.tsx @@ -62,6 +62,11 @@ export const Keys: React.FC = () => { const [selectedVerifiers, setSelectedVerifiers] = useState(); const [verifiersDialogOpen, setVerifiersDialogOpen] = useState(false); + useEffect(() => { + setFilter(searchParams.get('filter') ?? undefined); + setParent(searchParams.get('path') ?? ''); + }, [searchParams]); + const { data: keys, error } = useQuery({ queryKey: ["keys", parent, sortBy, sortOrder, refEntries, rowsPerPage, filter], queryFn: () => fetchKeys(parent, rowsPerPage, sortBy, sortOrder, filter, refEntries[refEntries.length - 1]) From 1db751224fda178fe340756e9211d2eacfc0920e Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Fri, 31 Jan 2025 09:56:10 -0500 Subject: [PATCH 09/10] Updates based on PR feedback Signed-off-by: Gabriel Indik --- ui/client/src/App.tsx | 2 +- ui/client/src/components/Hash.tsx | 2 +- ui/client/src/components/config.ts | 4 +++- ui/client/src/dialogs/ReverseKeyLookup.tsx | 5 +++-- ui/client/src/views/{indexer.tsx => indexerView.tsx} | 0 5 files changed, 8 insertions(+), 5 deletions(-) rename ui/client/src/views/{indexer.tsx => indexerView.tsx} (100%) diff --git a/ui/client/src/App.tsx b/ui/client/src/App.tsx index 6aa86f626..31565d724 100644 --- a/ui/client/src/App.tsx +++ b/ui/client/src/App.tsx @@ -26,7 +26,7 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { Header } from "./components/Header"; import { ApplicationContextProvider } from "./contexts/ApplicationContext"; import { darkThemeOptions, lightThemeOptions } from "./themes/default"; -import { Indexer } from "./views/Indexer"; +import { Indexer } from "./views/indexerView"; import { Registries } from "./views/Registries"; import { Submissions } from "./views/Submissions"; import { useEffect, useMemo, useState } from "react"; diff --git a/ui/client/src/components/Hash.tsx b/ui/client/src/components/Hash.tsx index a0045f6b5..382e78ce7 100644 --- a/ui/client/src/components/Hash.tsx +++ b/ui/client/src/components/Hash.tsx @@ -1,4 +1,4 @@ -// Copyright © 20245 Kaleido, Inc. +// Copyright © 2025 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/ui/client/src/components/config.ts b/ui/client/src/components/config.ts index 5ea712209..75487eec5 100644 --- a/ui/client/src/components/config.ts +++ b/ui/client/src/components/config.ts @@ -24,5 +24,7 @@ export const constants = { REGISTRY_ENTRIES_QUERY_LIMIT: 100, TRANSACTION_QUERY_LIMIT: 10, UPDATE_FREQUENCY_MILLISECONDS: 5000, - ELLAPSED_TIME_AUTO_REFRESH_FREQUENCY_SECONDS: 60 + ELLAPSED_TIME_AUTO_REFRESH_FREQUENCY_SECONDS: 60, + KEY_ETHEREUM_TYPE: 'eth_address', + KEY_ETHEREUM_ALGORITHM: 'ecdsa:secp256k1' }; diff --git a/ui/client/src/dialogs/ReverseKeyLookup.tsx b/ui/client/src/dialogs/ReverseKeyLookup.tsx index e0b1f5734..6222adc35 100644 --- a/ui/client/src/dialogs/ReverseKeyLookup.tsx +++ b/ui/client/src/dialogs/ReverseKeyLookup.tsx @@ -31,6 +31,7 @@ import { useQuery } from '@tanstack/react-query'; import { t } from 'i18next'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { reverseKeyLookup } from '../queries/keys'; +import { constants } from '../components/config'; type Props = { dialogOpen: boolean @@ -75,8 +76,8 @@ export const ReverseKeyLookupDialog: React.FC = ({ }, [dialogOpen]); useEffect(() => { - setType(isEthereum ? 'eth_address' : otherType); - setAlgorithm(isEthereum ? 'ecdsa:secp256k1' : otherAlgorithm); + setType(isEthereum ? constants.KEY_ETHEREUM_TYPE : otherType); + setAlgorithm(isEthereum ? constants.KEY_ETHEREUM_ALGORITHM : otherAlgorithm); }, [isEthereum, otherType, otherAlgorithm]); const handleSubmit = () => { diff --git a/ui/client/src/views/indexer.tsx b/ui/client/src/views/indexerView.tsx similarity index 100% rename from ui/client/src/views/indexer.tsx rename to ui/client/src/views/indexerView.tsx From f8b8ad3cb203fbe78d3adda892b8bf63d1e3e620 Mon Sep 17 00:00:00 2001 From: Gabriel Indik Date: Fri, 31 Jan 2025 09:56:45 -0500 Subject: [PATCH 10/10] Address case change in file name Signed-off-by: Gabriel Indik --- ui/client/src/App.tsx | 2 +- ui/client/src/views/{indexerView.tsx => Indexer.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename ui/client/src/views/{indexerView.tsx => Indexer.tsx} (100%) diff --git a/ui/client/src/App.tsx b/ui/client/src/App.tsx index 31565d724..6aa86f626 100644 --- a/ui/client/src/App.tsx +++ b/ui/client/src/App.tsx @@ -26,7 +26,7 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { Header } from "./components/Header"; import { ApplicationContextProvider } from "./contexts/ApplicationContext"; import { darkThemeOptions, lightThemeOptions } from "./themes/default"; -import { Indexer } from "./views/indexerView"; +import { Indexer } from "./views/Indexer"; import { Registries } from "./views/Registries"; import { Submissions } from "./views/Submissions"; import { useEffect, useMemo, useState } from "react"; diff --git a/ui/client/src/views/indexerView.tsx b/ui/client/src/views/Indexer.tsx similarity index 100% rename from ui/client/src/views/indexerView.tsx rename to ui/client/src/views/Indexer.tsx