Skip to content

Commit

Permalink
Merge pull request #537 from LF-Decentralized-Trust-labs/key-explorer-ui
Browse files Browse the repository at this point in the history
Key explorer UI
  • Loading branch information
gabriel-indik authored Feb 7, 2025
2 parents 818560b + f9fa24b commit 8f2d97c
Show file tree
Hide file tree
Showing 16 changed files with 789 additions and 16 deletions.
4 changes: 3 additions & 1 deletion ui/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({}),
Expand Down Expand Up @@ -99,6 +100,7 @@ function App() {
<Route path={AppRoutes.Indexer} element={<Indexer />} />
<Route path={AppRoutes.Submissions} element={<Submissions />} />
<Route path={AppRoutes.Peers} element={<Nodes />} />
<Route path={AppRoutes.Keys} element={<Keys />} />
<Route path={AppRoutes.Registry} element={<Registries />} />
<Route path="*" element={<Navigate to={AppRoutes.Indexer} replace />} />
</Routes>
Expand Down
13 changes: 8 additions & 5 deletions ui/client/src/components/Hash.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2024 Kaleido, Inc.
// Copyright © 2025 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -23,20 +23,23 @@ const MAX_LENGTH_WITHOUT_COLLAPSE = 16;
type Props = {
Icon?: JSX.Element
title: string
hideTitle?: boolean
hash: string
secondary?: boolean
}

export const Hash: React.FC<Props> = ({ Icon, title, hash }) => {
export const Hash: React.FC<Props> = ({ Icon, title, hideTitle, hash, secondary }) => {

const [hashDialogOpen, setHashDialogOpen] = useState(false);

const getHash = (value: string) => {
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)}`

return (
<>
Expand All @@ -46,10 +49,10 @@ export const Hash: React.FC<Props> = ({ Icon, title, 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">
{`${title} | ${getHash(hash)}`}
{content}
</Button>
<HashDialog dialogOpen={hashDialogOpen} setDialogOpen={setHashDialogOpen} title={title} hash={hash} />
</>
Expand Down
8 changes: 6 additions & 2 deletions ui/client/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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;
}
};

Expand All @@ -77,6 +80,7 @@ export const Header: React.FC = () => {
<Tab sx={{ textTransform: 'none' }} label={t('indexer')} />
<Tab sx={{ textTransform: 'none' }} label={t('submissions')} />
<Tab sx={{ textTransform: 'none' }} label={t('peers')} />
<Tab sx={{ textTransform: 'none' }} label={t('keys')} />
<Tab sx={{ textTransform: 'none' }} label={t('registry')} />
</Tabs>
</Grid2>
Expand Down
2 changes: 1 addition & 1 deletion ui/client/src/components/Transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const Transactions: React.FC = () => {
hasMore={hasNextPage}
loader={<LinearProgress />}
>
{transactions.pages.map(transactionArrat => transactionArrat?.map((transaction) => (
{transactions.pages.map(transactionArray => transactionArray.map((transaction) => (
<Transaction
key={transaction.hash}
transaction={transaction}
Expand Down
7 changes: 6 additions & 1 deletion ui/client/src/components/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@

export const constants = {
COLOR_MODE_STORAGE_KEY: 'color-mode',
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,
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'
};
193 changes: 193 additions & 0 deletions ui/client/src/dialogs/ReverseKeyLookup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// 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,
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';
import { constants } from '../components/config';

type Props = {
dialogOpen: boolean
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>
setParent: Dispatch<SetStateAction<string>>
setFilter: Dispatch<SetStateAction<string | undefined>>
}

export const ReverseKeyLookupDialog: React.FC<Props> = ({
dialogOpen,
setDialogOpen,
setParent,
setFilter
}) => {

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 ? constants.KEY_ETHEREUM_TYPE : otherType);
setAlgorithm(isEthereum ? constants.KEY_ETHEREUM_ALGORITHM : otherAlgorithm);
}, [isEthereum, otherType, otherAlgorithm]);

const handleSubmit = () => {
refetch().then(result => {
if (result.status === 'success') {
const path = result.data.path.map(segment => segment.name).join('.');
const index = path.lastIndexOf('.');
if(index !== -1) {
setParent(path.substring(0, index));
setFilter(path.substring(index + 1))
} else {
setFilter(path);
}
setDialogOpen(false);
} else if (result.status === 'error') {
setNotFound(true);
}
});
};

const canSubmit = verifier.length > 0 && (isEthereum ||
(otherType.length > 0 && otherAlgorithm.length > 0));

return (
<Dialog
disableRestoreFocus
fullWidth
open={dialogOpen}
maxWidth="sm"
onClose={() => setDialogOpen(false)}
>
<form onSubmit={(event) => {
event.preventDefault();
handleSubmit();
}}>
<DialogTitle sx={{ textAlign: 'center' }}>
{t('reverseLookup')}
{notFound &&
<Alert variant="filled" severity="warning">{t('verifierNotFound')}</Alert>}
</DialogTitle>
<DialogContent>
<Box sx={{ marginTop: '5px' }}>
<TextField
autoFocus
slotProps={{
inputLabel: { shrink: true },
}}
autoComplete="off"
fullWidth
label={t('address')}
value={verifier}
onChange={event => setVerifier(event.target.value)} />

<RadioGroup
sx={{ marginTop: '10px' }}
value={isEthereum ? 'ethereum' : 'other'}
onChange={event => setIsEthereum(event.target.value === 'ethereum')}
>
<FormControlLabel value="ethereum" control={<Radio />} label={t('ethereum')} />
<FormControlLabel value="other" control={<Radio />} label={t('other')} />
</RadioGroup>
<Box sx={{ marginTop: '15px', marginLeft: '30px' }}>
<TextField
slotProps={{
inputLabel: { shrink: true },
}}
autoComplete="off"
fullWidth
disabled={isEthereum}
label={t('type')}
value={otherType}
onChange={event => setOtherType(event.target.value)}>
</TextField>
<TextField
sx={{ marginTop: '20px' }}
slotProps={{
inputLabel: { shrink: true },
}}
autoComplete="off"
fullWidth
disabled={isEthereum}
label={t('algorithm')}
value={otherAlgorithm}
onChange={event => setOtherAlgorithm(event.target.value)}>
</TextField>
</Box>

</Box>
</DialogContent>
<DialogActions sx={{ justifyContent: 'center', paddingBottom: '20px' }}>
<Button
sx={{ minWidth: '100px' }}
size="large"
variant="contained"
disableElevation
disabled={!canSubmit}
type="submit">
{t('lookup')}
</Button>
<Button
sx={{ minWidth: '100px' }}
size="large"
variant="outlined"
disableElevation
onClick={() => setDialogOpen(false)}
>
{t('cancel')}
</Button>
</DialogActions>
</form>
</Dialog>
);
};
74 changes: 74 additions & 0 deletions ui/client/src/dialogs/Verifiers.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SetStateAction<boolean>>
verifiers: IVerifier[]
}

export const VerifiersDialog: React.FC<Props> = ({
dialogOpen,
setDialogOpen,
verifiers
}) => {

const { t } = useTranslation();

return (
<Dialog
fullWidth
open={dialogOpen}
onClose={() => setDialogOpen(false)}
maxWidth="md"
>
<DialogTitle sx={{ textAlign: 'center' }}>
{t('verifiers')}
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', gap: '30px', flexDirection: 'column' }}>
{verifiers.map(verifier =>
<Box key={verifier.verifier} sx={{ display: 'flex', gap: '10px', flexDirection: 'column' }}>
<SingleValue label={t('type')} value={verifier.type} />
<SingleValue label={t('algorithm')} value={verifier.algorithm} />
<SingleValue label={t('verifier')} value={verifier.verifier} />
</Box>)}
</Box>
</DialogContent>
<DialogActions sx={{ justifyContent: 'center', marginBottom: '15px' }}>
<Button
onClick={() => setDialogOpen(false)}
sx={{ textTransform: 'none' }}
variant="contained"
disableElevation>
{t('close')}
</Button>
</DialogActions>
</Dialog>
);
};
Loading

0 comments on commit 8f2d97c

Please sign in to comment.